refactor: adjust spacing and improve layout across multiple screens

This commit is contained in:
Resh 2024-12-15 20:50:15 +07:00
parent bbd26c0cb1
commit aae59d0a86
20 changed files with 282 additions and 186 deletions

View File

@ -21,6 +21,9 @@ class CustomButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final screenWidth = mediaQuery.size.width;
return SizedBox(
width: width,
height: height,
@ -46,7 +49,7 @@ class CustomButton extends StatelessWidget {
text,
style: textStyle ??
AppTextStyles.blackButtonTextStyle.copyWith(
fontSize: 14,
fontSize: screenWidth * 0.036,
fontWeight: FontWeight.w900,
),
),

View File

@ -50,20 +50,23 @@ class CustomFieldWidget extends StatefulWidget {
class _CustomFieldWidgetState extends State<CustomFieldWidget> {
late TextEditingController _controller;
late ValidatorProvider _validatorProvider;
@override
void initState() {
super.initState();
_controller = widget.controller ?? TextEditingController();
context
.read<ValidatorProvider>()
.setController(widget.fieldName, _controller);
// Simpan referensi provider di initState
_validatorProvider = context.read<ValidatorProvider>();
_validatorProvider.setController(widget.fieldName, _controller);
}
@override
void dispose() {
context.read<ValidatorProvider>().removeController(widget.fieldName);
// Gunakan referensi yang disimpan sebelumnya
_validatorProvider.removeController(widget.fieldName);
if (widget.controller == null) {
_controller.dispose();
}

View File

@ -160,6 +160,7 @@ class _ForgotPasswordScreenState extends State<ForgotPasswordScreen> {
),
],
),
SizedBox(height: screenHeight * 0.1)
],
);
}),

View File

@ -19,8 +19,9 @@ class SigninScreen extends StatefulWidget {
}
class _SigninScreenState extends State<SigninScreen> {
final TextEditingController _loginController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
late TextEditingController _loginController = TextEditingController();
late TextEditingController _passwordController = TextEditingController();
late ValidatorProvider _validatorProvider;
final FocusNode _loginFocus = FocusNode();
final FocusNode _passwordFocus = FocusNode();
final _formKey = GlobalKey<FormState>();
@ -28,16 +29,22 @@ class _SigninScreenState extends State<SigninScreen> {
@override
void initState() {
super.initState();
context.read<ValidatorProvider>().setController('login', _loginController);
context
.read<ValidatorProvider>()
.setController('password', _passwordController);
_loginController = TextEditingController();
_passwordController = TextEditingController();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_validatorProvider = context.read<ValidatorProvider>();
_validatorProvider.setController('login', _loginController);
_validatorProvider.setController('password', _passwordController);
}
@override
void dispose() {
context.read<ValidatorProvider>().removeController('login');
context.read<ValidatorProvider>().removeController('password');
_validatorProvider.removeController('login');
_validatorProvider.removeController('password');
_loginController.dispose();
_passwordController.dispose();
@ -346,7 +353,7 @@ class _SigninScreenState extends State<SigninScreen> {
),
],
),
SizedBox(height: screenHeight * 0.02),
SizedBox(height: screenHeight * 0.1),
],
);
},

View File

@ -7,10 +7,10 @@ class LoginEmailField extends StatefulWidget {
const LoginEmailField({super.key});
@override
_LoginEmailFieldState createState() => _LoginEmailFieldState();
LoginEmailFieldState createState() => LoginEmailFieldState();
}
class _LoginEmailFieldState extends State<LoginEmailField> {
class LoginEmailFieldState extends State<LoginEmailField> {
late FocusNode _focusNode;
@override

View File

@ -316,6 +316,7 @@ class _SignupScreenState extends State<SignupScreen> {
),
],
),
SizedBox(height: screenHeight * 0.1)
],
);
},

View File

@ -67,14 +67,18 @@ class HistoryProvider with ChangeNotifier {
Future<void> fetchLearningHistory(String token) async {
if (_sectionProvider.sections.isEmpty) {
_error = 'No sections available';
notifyListeners();
WidgetsBinding.instance.addPostFrameCallback((_) {
notifyListeners();
});
return;
}
String sectionId = _sectionProvider.sections[_selectedPageIndex].id;
_isLoading = true;
_error = null;
notifyListeners();
WidgetsBinding.instance.addPostFrameCallback((_) {
notifyListeners();
});
try {
final history = await _repository.getLearningHistory(sectionId, token);
@ -85,7 +89,9 @@ class HistoryProvider with ChangeNotifier {
_historyModel = [];
} finally {
_isLoading = false;
notifyListeners();
WidgetsBinding.instance.addPostFrameCallback((_) {
notifyListeners();
});
}
}

View File

@ -361,6 +361,9 @@ class _HomeContentState extends State<HomeContent> {
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final screenHeight = mediaQuery.size.height;
return RefreshIndicator(
onRefresh: _initializeData,
child: Consumer2<UserProvider, CompletedTopicsProvider>(
@ -486,7 +489,7 @@ class _HomeContentState extends State<HomeContent> {
return WelcomeCard(cardModel: cardData.cardData[index]);
},
options: CarouselOptions(
height: 168,
height: screenHeight * 0.19,
viewportFraction: 0.9,
enlargeCenterPage: true,
autoPlay: true,

View File

@ -69,7 +69,7 @@ class IncompleteSubmission extends StatelessWidget {
children: [
Expanded(
child: GlobalButton(
text: 'Check Again',
text: 'Check',
textColor: AppColors.blueColor,
borderColor: AppColors.blueColor,
backgroundColor: Colors.transparent,

View File

@ -1,6 +1,6 @@
import 'package:english_learning/core/services/repositories/level_repository.dart';
import 'package:english_learning/features/learning/modules/level/models/level_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
class LevelProvider with ChangeNotifier {
final LevelRepository _levelRepository = LevelRepository();
@ -21,8 +21,9 @@ class LevelProvider with ChangeNotifier {
Future<void> fetchLevels(String topicId, String token) async {
_isLoading = true;
_error = null;
notifyListeners();
WidgetsBinding.instance.addPostFrameCallback((_) {
notifyListeners();
});
try {
final result = await _levelRepository.getLevels(topicId, token);
_levels = result['levels'];
@ -37,7 +38,9 @@ class LevelProvider with ChangeNotifier {
_error = 'Error fetching levels: ${e.toString()}';
} finally {
_isLoading = false;
notifyListeners();
WidgetsBinding.instance.addPostFrameCallback((_) {
notifyListeners();
});
}
}

View File

@ -20,13 +20,18 @@ class LevelCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final screenHeight = mediaQuery.size.height;
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
child: Container(
height: MediaQuery.of(context).size.height * 0.6,
constraints: BoxConstraints(
maxHeight: screenHeight * 0.7,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: isAllowed
@ -107,20 +112,20 @@ class LevelCard extends StatelessWidget {
),
),
const SizedBox(height: 6),
// Score Display
Text(
// 'Score ${level.score}/100',
'Score $score/100',
style: AppTextStyles.whiteTextStyle.copyWith(
fontWeight: FontWeight.w900,
fontSize: 12,
),
),
const Spacer(), // Learn Now Button
const Spacer(),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: CustomButton(
text: isAllowed ? 'Learn Now' : 'Locked',
textStyle:

View File

@ -24,6 +24,9 @@ class PretestCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final screenHeight = mediaQuery.size.height;
return Consumer<LevelProvider>(builder: (context, levelProvider, _) {
final isFinished = levelProvider.isPretestFinished(pretest.idLevel);
final score = levelProvider.getPretestScore(pretest.idLevel);
@ -80,7 +83,9 @@ class PretestCard extends StatelessWidget {
children: [
Container(
padding: const EdgeInsets.symmetric(
vertical: 4, horizontal: 8),
vertical: 4,
horizontal: 8,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color:
@ -125,7 +130,7 @@ class PretestCard extends StatelessWidget {
Flexible(
child: Image.asset(
'lib/features/learning/modules/level/assets/images/pretest_level_illustration.png',
height: 95,
height: screenHeight * 0.13,
fit: BoxFit.cover,
),
),

View File

@ -5,22 +5,42 @@ import 'package:flutter/foundation.dart';
class SectionProvider extends ChangeNotifier {
final SectionRepository _repository = SectionRepository();
List<Section> _sections = [];
final bool _isLoading = false;
bool _isLoading = false;
dynamic _error;
List<Section> get sections => _sections;
bool get isLoading => _isLoading;
String? get error => _error;
Future<List<Section>> fetchSections(String token) async {
void resetData() {
_sections = [];
_error = null;
_isLoading = true;
notifyListeners();
}
Future<void> fetchSections(String token) async {
try {
// Set loading state
_isLoading = true;
_error = null;
notifyListeners();
// Fetch sections
_sections = await _repository.getSections(token);
// Reset loading state
_isLoading = false;
_error = null;
notifyListeners();
return _sections;
} catch (e) {
_error = e.toString();
// Handle error
_isLoading = false;
_error = e;
notifyListeners();
return [];
// Rethrow the error to be handled by the caller
rethrow;
}
}
}

View File

@ -24,7 +24,9 @@ class _LearningScreenState extends State<LearningScreen>
@override
void initState() {
super.initState();
_initializeSections();
WidgetsBinding.instance.addPostFrameCallback((_) {
_initializeSections();
});
}
Future<void> _initializeSections() async {
@ -32,45 +34,91 @@ class _LearningScreenState extends State<LearningScreen>
final sectionProvider =
Provider.of<SectionProvider>(context, listen: false);
// Cek apakah sections sudah ada
if (sectionProvider.sections.isEmpty) {
try {
final token = await userProvider.getValidToken();
if (token != null) {
await sectionProvider.fetchSections(token);
}
} catch (e) {
rethrow;
} finally {
setState(() {
_isInitialLoading = false;
});
try {
// Reset data sebelum fetch
sectionProvider.resetData(); // Tambahkan method ini di SectionProvider
final token = await userProvider.getValidToken();
if (token != null) {
await sectionProvider.fetchSections(token);
}
} else {
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to load data: ${e.toString()}'),
action: SnackBarAction(
label: 'Retry',
onPressed: _initializeSections,
),
),
);
}
} finally {
setState(() {
_isInitialLoading = false;
});
}
}
Future<void> _refreshSections() async {
final userProvider = Provider.of<UserProvider>(context, listen: false);
final sectionProvider =
Provider.of<SectionProvider>(context, listen: false);
// Future<void> _refreshSections() async {
// final userProvider = Provider.of<UserProvider>(context, listen: false);
// final sectionProvider =
// Provider.of<SectionProvider>(context, listen: false);
try {
final token = await userProvider.getValidToken();
if (token != null) {
await sectionProvider.fetchSections(token);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to refresh sections: $e'),
backgroundColor: Colors.red,
),
);
}
// try {
// final token = await userProvider.getValidToken();
// if (token != null) {
// await sectionProvider.fetchSections(token);
// }
// } catch (e) {
// ScaffoldMessenger.of(context).showSnackBar(
// SnackBar(
// content: Text('Failed to refresh sections: $e'),
// backgroundColor: Colors.red,
// ),
// );
// }
// }
Widget _buildErrorWidget(String error) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: 48,
color: Colors.red[300],
),
const SizedBox(height: 16),
Text(
'Oops! Something went wrong',
style: AppTextStyles.blackTextStyle.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
error,
textAlign: TextAlign.center,
style: AppTextStyles.disableTextStyle.copyWith(fontSize: 14),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _initializeSections,
icon: const Icon(Icons.refresh),
label: const Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryColor,
foregroundColor: Colors.white,
),
),
],
),
);
}
@override
@ -115,35 +163,13 @@ class _LearningScreenState extends State<LearningScreen>
// Tampilkan error jika ada
if (sectionProvider.error != null) {
return RefreshIndicator(
onRefresh: _refreshSections,
child: ListView(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Error: ${sectionProvider.error}',
style: AppTextStyles.greyTextStyle,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _refreshSections,
child: const Text('Retry'),
)
],
),
),
],
),
);
return _buildErrorWidget(sectionProvider.error!);
}
// Tampilkan sections atau pesan jika kosong
if (sectionProvider.sections.isEmpty) {
return RefreshIndicator(
onRefresh: _refreshSections,
onRefresh: _initializeSections,
child: ListView(
children: [
Center(
@ -165,7 +191,7 @@ class _LearningScreenState extends State<LearningScreen>
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _refreshSections,
onPressed: _initializeSections,
child: const Text('Refresh'),
)
],
@ -178,7 +204,7 @@ class _LearningScreenState extends State<LearningScreen>
// Tampilkan daftar sections
return RefreshIndicator(
onRefresh: _refreshSections,
onRefresh: _initializeSections,
child: ListView.builder(
itemCount: sectionProvider.sections.length,
itemBuilder: (context, index) {

View File

@ -107,7 +107,7 @@ class _OnBoardingScreenState extends State<OnBoardingScreen> {
currentPage: _currentPage,
itemCount: controller.onBoardingData.length,
),
SizedBox(height: screenHeight * 0.2),
SizedBox(height: screenHeight * 0.08),
if (_currentPage == controller.onBoardingData.length - 1)
Column(
children: [

View File

@ -51,92 +51,92 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
),
),
),
body: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Consumer<ValidatorProvider>(
builder: (context, validatorProvider, child) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 22.0,
body: SingleChildScrollView(child: Consumer<ValidatorProvider>(
builder: (
context,
validatorProvider,
child,
) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 22.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CustomFieldWidget(
isRequired: true,
labelText: 'Old Password',
hintText: 'Enter your old password',
textInputAction: TextInputAction.next,
fieldName: 'Old Password',
obscureText: true,
controller: _oldPasswordController,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CustomFieldWidget(
isRequired: true,
labelText: 'Old Password',
hintText: 'Enter your old password',
textInputAction: TextInputAction.next,
fieldName: 'Old Password',
obscureText: true,
controller: _oldPasswordController,
),
const SizedBox(height: 12),
CustomFieldWidget(
isRequired: true,
labelText: 'New Password',
hintText: 'Enter your new password',
textInputAction: TextInputAction.next,
fieldName: 'new password',
obscureText: true,
controller: _newPasswordController,
),
const SizedBox(height: 12),
CustomFieldWidget(
isRequired: true,
labelText: 'Confirm New Password',
hintText: 'Retype your new password',
textInputAction: TextInputAction.next,
fieldName: 'confirm new password',
obscureText: true,
controller: _confirmPasswordController,
),
const SizedBox(height: 24),
GlobalButton(
text: 'Update Now',
onPressed: () async {
final userProvider =
Provider.of<UserProvider>(context, listen: false);
bool success = await userProvider.updatePassword(
oldPassword: _oldPasswordController.text,
newPassword: _newPasswordController.text,
confirmPassword: _confirmPasswordController.text,
);
const SizedBox(height: 12),
CustomFieldWidget(
isRequired: true,
labelText: 'New Password',
hintText: 'Enter your new password',
textInputAction: TextInputAction.next,
fieldName: 'new password',
obscureText: true,
controller: _newPasswordController,
),
const SizedBox(height: 12),
CustomFieldWidget(
isRequired: true,
labelText: 'Confirm New Password',
hintText: 'Retype your new password',
textInputAction: TextInputAction.next,
fieldName: 'confirm new password',
obscureText: true,
controller: _confirmPasswordController,
),
const SizedBox(height: 24),
GlobalButton(
text: 'Update Now',
onPressed: () async {
final userProvider =
Provider.of<UserProvider>(context, listen: false);
bool success = await userProvider.updatePassword(
oldPassword: _oldPasswordController.text,
newPassword: _newPasswordController.text,
confirmPassword: _confirmPasswordController.text,
);
if (success) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return ChangePasswordDialog(
onSubmit: () {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) =>
const SigninScreen()),
(route) => false,
);
},
if (success) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return ChangePasswordDialog(
onSubmit: () {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => const SigninScreen()),
(route) => false,
);
},
);
} else {
CustomSnackBar.show(
context,
message:
'Failed to update password. Please try again.',
isError: true,
);
}
},
)
],
),
);
},
)),
},
);
} else {
CustomSnackBar.show(
context,
message: 'Failed to update password. Please try again.',
isError: true,
);
}
},
)
],
),
);
},
)),
);
}
}

View File

@ -89,6 +89,9 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final screenHeight = mediaQuery.size.height;
return Scaffold(
appBar: AppBar(
elevation: 0,
@ -196,6 +199,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
],
),
),
SizedBox(height: screenHeight * 0.05),
],
));
}),

View File

@ -84,7 +84,7 @@ class LogoutConfirmation extends StatelessWidget {
const SizedBox(width: 10),
Expanded(
child: GlobalButton(
text: 'Yes, logout!',
text: 'Yes',
onPressed: onSubmit,
textColor: AppColors.whiteColor,
backgroundColor: AppColors.redColor,

View File

@ -25,6 +25,9 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> {
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final screenHeight = mediaQuery.size.height;
final screenWidth = mediaQuery.size.width;
return Scaffold(
backgroundColor: AppColors.bgSoftColor,
body: Consumer<UserProvider>(builder: (context, userProvider, child) {
@ -35,7 +38,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
Container(
width: double.infinity,
height: 210,
height: screenHeight * 0.33,
decoration: BoxDecoration(
gradient: AppColors.gradientTheme,
borderRadius: const BorderRadius.only(
@ -44,8 +47,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
child: Padding(
padding: const EdgeInsets.only(
top: 71.0, left: 26, right: 16.0, bottom: 19.0),
padding: EdgeInsets.only(
top: screenHeight * 0.1,
left: screenWidth * 0.07,
right: screenWidth * 0.04,
bottom: screenHeight * 0.03,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -54,7 +61,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Row(
children: [
UserAvatar(
radius: 60,
radius: screenWidth * 0.15,
pictureUrl: userProvider.userData?['PICTURE'],
baseUrl: '${mediaUrl}avatar/',
onImageSelected: (File image) {
@ -63,7 +70,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
selectedImage: userProvider.selectedImage,
isLoading: userProvider.isLoading,
),
const SizedBox(width: 28),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -72,7 +79,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
'Loading...',
style:
AppTextStyles.whiteTextStyle.copyWith(
fontSize: 18,
fontSize: screenWidth * 0.05,
fontWeight: FontWeight.bold,
),
),
@ -82,9 +89,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
'Loading...',
style:
AppTextStyles.whiteTextStyle.copyWith(
fontSize: 14,
fontSize: screenWidth * 0.036,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),

View File

@ -97,7 +97,7 @@ class WelcomeScreen extends StatelessWidget {
),
],
),
SizedBox(height: screenHeight * 0.1),
SizedBox(height: screenHeight * 0.05),
],
),
),