Reputation: 1
This is an explanation of a stepper form with multiple pages, where data entered on earlier pages is passed to the final review page.
On this page, the user provides personal details:
First Name (e.g., "Tom")
Last Name (e.g., "Dsouza")
Select Country, Select Language, and Select Role
The data is saved temporarily to be used on subsequent pages.
Here, the user uploads a profile picture. This data doesn't directly affect the review page but is stored with the user's profile.
The user provides workspace information, such as:
Company Name (e.g., "Google")
Select Team Size
This data is stored for display on the final review page.
On the review page, the user sees a summary of the data entered:
Personal Details: First Name: Tom, Last Name: Dsouza, Country, Language, Role.
Workspace Details: Company Name: Google, Team Size.
The user can review and submit or make changes.
class OnboardingScreen extends StatefulWidget {
final OnboardingApiService? apiService;
const OnboardingScreen({super.key, required this.apiService,});
@override
_OnboardingScreenState createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends State<OnboardingScreen> {
final PageController _pageController = PageController();
final _formKey = GlobalKey<FormState>();
final TextEditingController firstNameController = TextEditingController();
final TextEditingController lastNameController = TextEditingController();
final TextEditingController workspaceController = TextEditingController();
final TextEditingController companyNameController = TextEditingController();
final TextEditingController phoneNumberController = TextEditingController();
final TextEditingController positionController = TextEditingController();
String? _selectedTeamSize;
String? selectedCountry;
String? selectedLanguage;
String? selectedRole;
File? _imageFile;
String? _profilePictureBase64="";
int _currentStep = 0;
final List<Step> _steps = [];
final List<bool> _stepRequiresValidation = [true, false, true, true];
@override
void initState() {
super.initState();
_steps.addAll([
Step(
title: const Text('Step 1 of 4'),
content: _personalInfoForm(),
),
Step(
title: const Text('Step 2 of 4'),
content: _profilePictureForm(),
),
Step(
title: const Text('Step 3 of 4'),
content: _workspaceDetailsForm(),
),
Step(
title: const Text('Step 4 of 4'),
content: _reviewForm(),
),
]);
fetchOnboarding();
}
// Function to pick an image from the gallery
Future<void> _pickImage() async {
final ImagePicker picker = ImagePicker();
final pickedFile = await picker.pickImage(source: ImageSource.gallery); setState(() { _imageFile = File(pickedFile?.path ?? ''); });
}
/// For Step Forward
void _nextStep() {
if ( !_stepRequiresValidation[_currentStep] || _formKey.currentState != null && _formKey.currentState!.validate()) {
if (_currentStep < _steps.length - 1) {
setState(() {
_currentStep++;
});
_pageController.nextPage(
duration: const Duration(milliseconds: 300), curve: Curves.ease);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: Colors.red,
content: Text('Please complete all required fields'),
),
);
}
}
}
/// For Step Backward
void _previousStep() {
if (_currentStep > 0) {
setState(() {
_currentStep--;
});
_pageController.previousPage(
duration: const Duration(milliseconds: 300), curve: Curves.ease);
}
}
double _getProgress() {
return (_currentStep + 1) / _steps.length;
}
Future<void> fetchOnboarding() async {
try {
OnboardingApiService apiService = OnboardingApiService();
OnboardingModel? data =
await apiService.fetchOnboardingData(widget.userToken);
if (data != null) {
setState(() {
firstNameController.text = data.firstName;
lastNameController.text = data.lastName;
phoneNumberController.text = data.number;
selectedRole = data.role;
selectedCountry = data.country;
selectedLanguage = data.language;
companyNameController.text = data.companyName;
_selectedTeamSize = data.teamSize.toString();
positionController.text = data.position;
});
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error fetching data: $e')),
);
}
}
Future<void> _submitForm() async {
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
await submitOnboarding();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: Colors.red,
content: Text('Please complete all required fields'),
),
);
}
}
Future<void> submitOnboarding() async {
OnboardingApiService apiService = OnboardingApiService();
OnboardingModel data = OnboardingModel(
firstName: firstNameController.toString(),
lastName: lastNameController.toString(),
profilePicture: _imageFile?.path ?? '',
role: selectedRole ?? '',
number: phoneNumberController.toString(),
country: selectedCountry ?? '',
language: selectedLanguage ?? '',
companyName: companyNameController.toString(),
teamSize: int.tryParse(_selectedTeamSize ?? '0') ?? 0,
position: positionController.toString(),
);
bool success = await apiService.submitOnboardingData(widget.userToken, data);
if(success == true){
await TokenStorage.saveUserState(isLoggedIn: true, isOnboardingCompleted: true);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
backgroundColor: Colors.green,
content: Text('Onboarding completed successfully!')),
);
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (ctx) => const HomeScreen()),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Onboarding submission failed.')),
);
}
}
Widget _personalInfoForm() {
return SingleChildScrollView(
child: Center(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(10.0),
vertical: 10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 4,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'First Name',
style: TextStyle(fontWeight: FontWeight.w500),
),
TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
controller: firstNameController,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'First name is required';
}
if (value.length < 2) {
return 'First name must be at least 2 characters';
}
return null;
},
),
const Text(
'Last Name',
style: TextStyle(fontWeight: FontWeight.w500),
),
TextFormField(
controller: lastNameController,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Last name is required';
}
if (value.length < 2) {
return 'Last name must be at least 2 characters';
}
return null;
},
),
const Text(
'Country',
style: TextStyle(fontWeight: FontWeight.w500),
),
DropdownButtonFormField<String>(
autovalidateMode: AutovalidateMode.onUserInteraction,
value: selectedCountry,
hint: const Text('Select your country'),
items: <String>['India', 'Usa', 'Brazil', 'Russia']
.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
selectedCountry = newValue;
});
},
validator: (value) =>
value == null ? 'Please select a country' : null,
),
const SizedBox(height: 15),
const Text(
'Language',
style: TextStyle(fontWeight: FontWeight.w500),
),
DropdownButtonFormField<String>(
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(11.0),
),
),
value: selectedLanguage,
hint: const Text('Select your language'),
items: <String>['Marathi', 'Hindi', 'English']
.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
selectedLanguage = newValue;
});
},
validator: (value) =>
value == null ? 'Please select a language' : null,
),
const Text(
'Phone Number (Optional)',
style: TextStyle(fontWeight: FontWeight.w500),
),
TextFormField(
controller: phoneNumberController,
autovalidateMode: AutovalidateMode.onUserInteraction,
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Phone number is required';
}
if (value.length < 10) {
return 'Please enter 10 digit number';
}
return null;
},
),
const Text(
'Role',
style: TextStyle(fontWeight: FontWeight.w500),
),
DropdownButtonFormField<String>(
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(11.0),
),
),
value: selectedRole,
hint: const Text('Select your role'),
items: <String>[
'Project Owner',
'Project Designer (Architect/ Interior)',
'Project Consultant',
]
.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
selectedLanguage = newValue;
});
},
validator: (value) =>
value == null ? 'Please select a language' : null,
),
SizedBox(
child: ElevatedButton(
onPressed: _nextStep,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF673ab7),
),
child: const Text(
'Continue',
style: TextStyle(color: Colors.white),
),
),
),
],
),
),
),
), // end of outer Column
],
),
),
),
),
);
}
////---------------------------WorkspaceDetails ----------------------
Widget _workspaceDetailsForm() {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Form(
key: _formKey,
child:Center(
child: Container(
padding: const EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 4,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 30),
const Center(
child: Text(
'Workspace Details',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF673ab7),
),
),
),
const Center(
child: Text(
'Tell us about your work environment',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
),
const Text(
'Company Name',
style: TextStyle(fontWeight: FontWeight.w500),
),
TextFormField(
controller: companyNameController,
decoration: const InputDecoration(
hintText: 'Acme Inc',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Company Name is required';
}
if (value.length < 2) {
return 'Company name must be at least 2 characters';
}
return null;
},
),
const Text(
'Team Size',
style: TextStyle(fontWeight: FontWeight.w500),
),
DropdownButtonFormField<String>(
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
value: _selectedTeamSize,
hint: const Text('Select your team size'),
items: <String>[
'Just me',
'2-5 people',
'6-10 people',
'11-20 people',
'21-50 people',
'50+ people'
].map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
_selectedTeamSize = newValue;
});
},
validator: (value) =>
value == null ? 'Please select a team size' : null,
),
const Text(
'Your Position',
style: TextStyle(fontWeight: FontWeight.w500),
),
TextFormField(
controller: positionController,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Your Position is required';
}
if (value.length < 2) {
return 'Position must be at least 2 characters';
}
return null;
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: _previousStep,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 25, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(9.0))),
child: const Text(
'Back',
style: TextStyle(color: Colors.black),
),
),
ElevatedButton(
onPressed: _nextStep,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 25, vertical: 15),
backgroundColor: const Color(0xFF673ab7),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(9.0))),
child: const Text(
'Continue',
style: TextStyle(color: Colors.white),
),
),
],
),
],
),
),
),
),
),
],
),
);
}
Widget _reviewForm() {
//return ReviewForm();
return SingleChildScrollView(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 243, 242, 242),
borderRadius: BorderRadius.circular(10.0)),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Personal Details',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 15),
Text('First Name: ${firstNameController.text}'),
const SizedBox(height: 5),
Text('Last Name: ${lastNameController.value.text.toString()}'),
const SizedBox(height: 5),
Text('Company Name: ${companyNameController.text}'),
const SizedBox(height: 5),
Text('Your Position: ${positionController.text}'),
],
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: _previousStep,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 25, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(9.0))),
child: const Text(
'Back',
style: TextStyle(color: Colors.black),
)),
ElevatedButton(
onPressed: () {
_submitForm();
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 25, vertical: 15),
backgroundColor: const Color(0xFF673ab7),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(9.0),
),
),
child: const Text(
'Complete Setup',
style: TextStyle(color: Colors.white),
),
),
],
),
],
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 45),
const Text(
'Welcome Aboard',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color(0xFF673ab7),
),
),
const SizedBox(height: 8),
const Text(
"Let's personalize your experience",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
const SizedBox(height: 16),
LinearProgressIndicator(
minHeight: 10,
value: _getProgress(),
backgroundColor: Colors.grey[300],
color: const Color(0xFF673ab7),
borderRadius: BorderRadius.circular(11.0),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Step ${_currentStep + 1}/4',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
),
Text(
'${(_getProgress() * 100).toInt()}% completed',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
),
],
),
const SizedBox(height: 8),
Flexible(
child: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: _steps.map((step) => step.content).toList(),
),
),
],
),
),
);
}
}
Upvotes: 0
Views: 22