Related
I a beginner in Flutter development and I was trying to replicate an animated login screen from this LinkedIn post. I am using Rive animations to make all the states required for the screen possible. A base rive file was available for the screen and after some tweaks, I was able to get the required states.
After implementing the animations in the screen along with two text fields, I noticed an issue with the animations. Whenever I click on the TextField, the soft keyboard pops up. The required animation runs for a short duration then resets to the idle animation. After digging through stack overflow and google and printing debug messages, I noticed that the whole widget rebuilds itself whenever soft keyboard pops up.
Here's the code for my screen -
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
#override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
StateMachineController? _controller;
SMIInput<bool>? isChecking;
SMIInput<bool>? trigSuccess;
SMIInput<bool>? trigFail;
SMIInput<bool>? isHandsUp;
SMIInput<double>? numLook;
FocusNode emailFocusNode = FocusNode();
TextEditingController emailController = TextEditingController();
FocusNode passwordFocusNode = FocusNode();
TextEditingController passController = TextEditingController();
#override
void initState() {
super.initState();
emailFocusNode.addListener(emailFocus);
passwordFocusNode.addListener(passFocus);
}
#override
void dispose() {
super.dispose();
emailFocusNode.removeListener(emailFocus);
passwordFocusNode.removeListener(passFocus);
}
void emailFocus() {
isChecking?.change(emailFocusNode.hasFocus);
}
void passFocus() {
isHandsUp?.change(passwordFocusNode.hasFocus);
}
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Scaffold(
resizeToAvoidBottomInset: true,
backgroundColor: Color(0xffeef2f3),
// Colors.lightBlueAccent.withBlue(255).withGreen(242).withRed(238),
body: SafeArea(
child: SingleChildScrollView(
child: Container(
width: size.width,
height: size.height,
child: Center(
child: Container(
height: 600,
width: 320,
decoration: BoxDecoration(
color: Color(0xFFffffff),
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 5,
),
],
),
child: Padding(
padding: EdgeInsets.symmetric(
vertical: 25,
horizontal: 25,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CircleAvatar(
backgroundColor: Colors.black87,
radius: 90,
child: CircleAvatar(
backgroundColor: Colors.transparent,
radius: 88,
child: ClipOval(
child: RiveAnimation.asset(
'assets/login_screen_character.riv',
stateMachines: ["Login Machine"],
onInit: (Artboard artboard) {
_controller =
StateMachineController.fromArtboard(
artboard, "Login Machine");
if (_controller == null) return;
artboard.addController(_controller!);
isChecking =
_controller?.findInput("isChecking");
trigSuccess =
_controller?.findInput("trigSuccess");
trigFail = _controller?.findInput("trigFail");
isHandsUp = _controller?.findInput("isHandsUp");
numLook = _controller?.findInput("numLook");
},
),
),
),
),
SizedBox(
height: 32,
),
Text(
"Email",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xff32779d),
),
),
SizedBox(
height: 4,
),
Container(
decoration: BoxDecoration(
color: Color(0x321fb0d2),
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: Color(0xff1fb0d2),
),
),
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"email#example.com",
style: TextStyle(
fontSize: 12,
color: Color(0xff32779d),
),
),
TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Enter email",
),
focusNode: emailFocusNode,
controller: emailController,
onChanged: (value) {
if (isChecking?.value == false)
isChecking?.change(true);
numLook?.change(value.length.toDouble() * 3);
},
),
],
),
),
),
SizedBox(
height: 32,
),
Text(
"Password",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xff32779d),
),
),
SizedBox(
height: 2,
),
Container(
decoration: BoxDecoration(
color: Color(0x321fb0d2),
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: Color(0xff1fb0d2),
),
),
child: Padding(
padding: const EdgeInsets.only(
left: 16.0,
right: 16.0,
top: 4,
bottom: 4,
),
child: TextField(
onChanged: (value) {
if (isHandsUp?.value == false)
isHandsUp?.change(true);
},
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Enter password",
),
focusNode: passwordFocusNode,
controller: passController,
),
),
),
SizedBox(
height: 32,
),
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Color(0xff1fb0d2),
),
),
onPressed: () {
if (isHandsUp?.value == true)
isHandsUp?.change(false);
Future.delayed(Duration(milliseconds: 1200), () {
if (passController.text == "thisisatest")
trigSuccess?.change(true);
else
trigFail?.change(true);
});
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
"Log In",
style: TextStyle(
color: Colors.white,
),
),
),
)
],
),
),
),
),
),
),
),
);
}
}
The code is a bit too long, please excuse me for that I haven't really refactored the code.
I've tried using the resizeToAvoidBottomInset: false but animation still resets to the idle state. I've even tried adding the android:windowSoftInputMode="adjustNothing" inside the <activity...> tag but still no result.
I would really like to know if there's any workaround in my code to make the animation states run properly.
tags in my code is a List which is comes from previous page,
I want to be able to remove and add items to it in my current page,
first I tried to initialize it in my initState but when I want to remove tags it doesn't work.
I guessed it is about the initialization, so I removed it from initState but now I am getting this error message Field 'tags' has not been initialized. I will attach my full code and I appreciate any suggestions about it. Thanks.
PS: I tried initializing tags with tags=[] in my initState but I can't remove any of the tags. It has a GestureDetector to remove tags, when I press it nothing happens. I guess initializing in the build function causes it, but I don't know where to initialize it that I could be able to edit it while the list is coming from another screen.
also when I added tags = widget.tags.where((e) => e != null && e != "").toList(); to my initiState and I inspect tags value, there is an empty string in addtion to other values at the end of the list.
import 'dart:convert';
import 'dart:developer';
import 'package:double_back_to_close/toast.dart';
import 'package:flutter/material.dart';
import 'package:pet_store/main.dart';
import 'package:pet_store/widgets/tag_preview.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'dart:async';
import 'dart:math';
import 'package:http/http.dart' as http;
import 'package:pet_store/widgets/tag_retrieve_preview.dart';
import 'utils/utils.dart';
class update_pet extends StatefulWidget {
var id;
var name;
var status;
var category;
var tags ;
update_pet(
{this.id, this.name, this.status, this.category, this.tags, Key? key})
: super(key: key);
#override
_update_petState createState() => _update_petState();
}
class _update_petState extends State<update_pet> {
final _picker = ImagePicker();
Future<void> _openImagePicker() async {
final XFile? pickedImage =
await _picker.pickImage(source: ImageSource.gallery);
if (pickedImage != null) {
setState(() {
final bytes = File(pickedImage.path).readAsBytesSync();
base64Image = "data:image/png;base64," + base64Encode(bytes);
});
}
}
String? base64Image;
TextEditingController nameController = TextEditingController();
TextEditingController categoryController = TextEditingController();
TextEditingController tagController = TextEditingController();
late File imageFile;
late List<dynamic> tags;
bool isLoading = false;
void initState() {
super.initState();
nameController.text = widget.name;
categoryController.text = widget.category;
addTag();
}
void addTag() {
setState(() {
tags.add(tagController.text);
tagController.clear();
});
}
String? dropdownvalue;
var items = [
'available',
'pending',
'sold',
];
#override
Widget build(BuildContext context) {
tags = widget.tags.where((e) => e != null && e != "").toList();
// inspect(tags);
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.indigo,
title: const Text('Update a pet'),
leading: GestureDetector(
child: Icon(
Icons.arrow_back_ios,
color: Colors.white,
),
onTap: () {
// Navigator.pop(context);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
PetStoreHomePage(LoggedIn: true),
),
(route) => false,
);
},
),
),
body: GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Card(
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
children: [
const ListTile(
// leading,
title: Text(
"General Information",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
TextField(
controller: nameController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "Enter your pet's name",
),
),
DropdownButton(
hint: const Text('Select Status'),
value: dropdownvalue,
icon: const Icon(Icons.keyboard_arrow_down),
items: items.map((String items) {
return DropdownMenuItem(
value: items,
child: Text(items),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
dropdownvalue = newValue!;
});
},
),
],
),
),
elevation: 8,
shadowColor: Colors.grey.shade300,
margin: const EdgeInsets.all(20),
shape: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.white)),
),
Card(
child: Container(
padding: const EdgeInsets.all(10),
child: Column(
children: [
const ListTile(
// leading,
title: Text(
"Category",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
TextField(
controller: categoryController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "Specify your pet's category",
),
),
],
),
),
elevation: 8,
shadowColor: Colors.grey.shade300,
margin:
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
shape: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.white)),
),
Card(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const ListTile(
// leading,
title: Text(
"Tags",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Flexible(
fit: FlexFit.loose,
child: ListView.builder(
shrinkWrap: true,
itemCount: tags.length,
itemBuilder: (_, index) {
print(tags);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 5.0, vertical: 3.0),
child: GestureDetector(
child: tagRetrievePreview(tags[index]),
onTap: () {
print("REMOVED "+tags[index]);
tags.removeAt(index);
}),
);
},
),
),
TextField(
controller: tagController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Specify your pet's tags",
suffixIcon: IconButton(
icon: const Icon(
Icons.add_circle_outline_sharp,
color: Color.fromARGB(255, 129, 128, 128),
size: 30,
),
onPressed: () {
addTag();
print(tags);
},
),
),
),
],
),
),
elevation: 8,
shadowColor: Colors.grey.shade300,
margin:
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
shape: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.white)),
),
Card(
child: Column(
children: [
const ListTile(
title: Text(
"Images",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
(base64Image != null)
? SizedBox(
width: 100,
height: 100,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
image: DecorationImage(
image: image(base64Image!).image,
fit: BoxFit.fill),
)),
)
: SizedBox(
width: 100,
height: 100,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: Colors.grey.shade300,
),
child: Icon(
Icons.camera_alt,
color: Colors.grey.shade800,
size: 30,
),
),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 100),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
const Color.fromARGB(255, 129, 128, 128)),
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 20)),
shape: MaterialStateProperty.all<
RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
side: const BorderSide(
color: Color.fromARGB(255, 129, 128, 128),
width: 2.0,
),
),
),
),
child: const Text(
'Select Image',
style:
TextStyle(fontSize: 15.0, color: Colors.white),
),
onPressed: _openImagePicker),
),
],
),
elevation: 8,
shadowColor: Colors.grey.shade300,
margin:
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
shape: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.white)),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 120),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.indigo),
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 40)),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0),
side: const BorderSide(
color: Colors.indigo,
width: 2.0,
),
),
),
),
child: isLoading
? const SizedBox(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white)),
height: 15.0,
width: 15.0,
)
: const Text(
'Update Pet',
style:
TextStyle(fontSize: 17.0, color: Colors.white),
),
onPressed: () async {
if (nameController.text.isEmpty) {
nameController.text = widget.name;
}
if (tagController.text.isEmpty) {
tagController.text = widget.tags;
}
if (categoryController.text.isEmpty) {
categoryController.text = widget.category;
}
if (dropdownvalue == null) {
dropdownvalue = widget.status;
}
print('Pressed');
Map data = {
"id": random.nextInt(10000),
///random it integer
"name": nameController.text,
"category": {
"id": set_category_id(categoryController.text),
"name": categoryController.text,
},
"photoUrls": [base64Image],
"tags": set_tags(tags),
"status": dropdownvalue
};
var body = json.encode(data);
var response = await http.put(
Uri.parse(
"https://api.training.testifi.io/api/v3/pet"),
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: body);
print(response.body);
print(response.statusCode);
if (response.statusCode == 201 ||
response.statusCode == 200) {
print('success');
Toast.show("Pet is successfully updated.", context);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (BuildContext context) =>
PetStoreHomePage(LoggedIn: true),
),
(route) => false,
);
} else {
Toast.show(
"ERROR! Updating pet failed. Please try again.",
context);
}
}),
),
],
),
),
),
);
}
}
tags = widget.tags.where((e) => e != null && e != "").toList();
This line in build method will constantly take value from widget.tags and create a list of items thats not null and empty.
Please add this in initState.
Just initialize it
List<dynamic> tags = [];
PS: I tried initializing tags with tags=[] in my initState but I can't
remove any of the tags.
You need to add setState to rebuild the page.
So I'm trying to update the value of the drop down list using the setState, but when I select a value from the list, the value on the screen won't be able to change
import 'package:demo_app_admin/Custom/custom_Font.dart';
import 'package:flutter/material.dart';
import '/Custom/Custom_Raised_Bottun.dart';
import '/Custom/Header.dart';
import '/Custom/inputField.dart';
class LoginView extends StatefulWidget {
#override
_LoginViewState createState() => _LoginViewState();
}
class _LoginViewState extends State<LoginView> {
#override
Widget build(BuildContext context) {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final isKeyboard = MediaQuery.of(context).viewInsets.bottom != 0;
String _dropdownvalue = "Global admin";
List<String> _items = <String> ["Global admin", "Institution admin"];
return Scaffold(
body: Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(begin: Alignment.topCenter, colors: [
Colors.teal[700]!,
Colors.teal[200]!,
Colors.teal[400]!
]),
),
child: Column(
children: <Widget>[
const SizedBox(
height: 80,
),
if (!isKeyboard)
Header(
padding: const EdgeInsets.all(20),
crossAxisAlignment: CrossAxisAlignment.start,
head: "Welcome",
headTextStyle:
const TextStyle(color: Colors.white, fontSize: 40),
sizedBoxHeight: 10,
subtitle: "Sign In to Your Admin Account",
subtitleTextStyle:
const TextStyle(color: Colors.white, fontSize: 18),
),
Expanded(
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(60),
topRight: Radius.circular(60),
)),
child: Padding(
padding: const EdgeInsets.all(30),
child: SingleChildScrollView(
reverse: true,
padding: EdgeInsets.all(32),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
const SizedBox(
height: 40,
),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10)),
child: Column(
children: <Widget>[
InputField(
labelText: 'Email',
padding: EdgeInsets.all(10),
borderSide: BorderSide(color: Colors.grey),
hintText: "Enter your email",
color: Colors.grey,
inputBorder: InputBorder.none,
obscureText: false,
enableSuggestion: true,
autoCorrect: true,
onSaved: (value) {},
// (value){controller.email = value!;},
validator: (value) {
if (value == null) {
print("ERROR");
}
},
),
InputField(
labelText: 'Password',
padding: EdgeInsets.all(10),
borderSide: BorderSide(color: Colors.grey),
hintText: "Enter your password",
color: Colors.grey,
inputBorder: InputBorder.none,
obscureText: true,
enableSuggestion: false,
autoCorrect: false,
onSaved: (value) {},
// (value){ controller.password = value!;},
validator: (value) {
if (value == null) {
print("ERROR");
}
},
),
],
),
),
const SizedBox(
height: 40,
),
here is the issue
DropdownButton<String>(
value: _dropdownvalue,
icon: Icon(Icons.keyboard_arrow_down),
items: _items.map((String _items) {
return DropdownMenuItem<String>(
value: _items,
child: CustomFont(text: _items),
);
}).toList(),
onChanged: (value) {
setState(() => _dropdownvalue = value!);
}),
const SizedBox(
height: 40,
),
CustomRaisedButton(
width: 200,
height: 50.0,
margin: const EdgeInsets.symmetric(horizontal: 50),
color: Colors.teal.shade500,
borderRadius: BorderRadius.circular(10),
text: "LOGIN",
textColor: Colors.white,
fontSize: 17,
fontWeight: FontWeight.bold,
function: () {}
// {
// _formKey.currentState?.save();
// if(_formKey.currentState?.validate() !=null){
// controller.signInWithEmailAndPassword();
// }
// },
),
SizedBox(
height: 10,
),
],
),
),
),
),
))
],
),
),
);
}
}
The issue is here, you are declaring variables inside build. Therefore, variables get reset to default value on every build. Means setState.
Declare variables outside the build method.
class _LoginViewState extends State<LoginView> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String _dropdownvalue = "Global admin";
List<String> _items = <String>["Global admin", "Institution admin"];
#override
Widget build(BuildContext context) {
final isKeyboard = MediaQuery.of(context).viewInsets.bottom != 0;
return Scaffold(
More about StatefulWidget.
I want to implement OTP verification screen without any package.
when i entered the number it should move to next input field
I m using this code in my current project take a refrence this will help
class Otp extends StatefulWidget {
final String? phnNumber;
final String ? code;
String? from;
Otp({Key ?key, this.phnNumber, this.from, this.code}) : super(key:
key);
#override
_OtpState createState() => _OtpState();
}
class _OtpState extends State<Otp> {
double ? height ;
double ? width;
TextEditingController ? contrller1;
TextEditingController ? contrller2;
TextEditingController ? contrller3;
TextEditingController ? contrller4;
SendOtpRequest resend = SendOtpRequest();
SharedPreferences ? prefs;
getSharedPreferences () async
{
prefs = await SharedPreferences.getInstance();
}
String Code = "";
#override
void initState() {
// TODO: implement initState
super.initState();
contrller1 = TextEditingController();
contrller2 = TextEditingController();
contrller3 = TextEditingController();
contrller4 = TextEditingController();
getSharedPreferences();
}
#override
Widget build(BuildContext context) {
height= MediaQuery.of(context).size.height;
width = MediaQuery.of(context).size.height;
final verifyprovider = Provider.of<PostDataProvider>(context);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
toolbarHeight:height! * 0.07802345,
titleSpacing: 0,
backgroundColor: HexColor("#18263d"),
automaticallyImplyLeading: false,
leading: Padding(
padding: const EdgeInsets.only(left: 8.0,),
child: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Container(
color: Colors.transparent,
child: Image.asset("assets/images/back_ic-1.png")),
),
),
// SizedBox(width: width!*0.001234,),
title:Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
height: height!/15,
width: height!/15,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 2,
color:HexColor("#fc4f00"),
)),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
height: height!/11,
width: height!/11,
decoration: BoxDecoration(
image: const DecorationImage(
image:
AssetImage("assets/images/home_logo.png"),
fit: BoxFit.fill
),
shape: BoxShape.circle,
border: Border.all(
width: 1,
color:HexColor("#fc4f00"),
)),
),
),
),
SizedBox(width: width! * 0.04234,),
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text("Verification",
style: GoogleFonts.oswald(fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: width! * 0.03345
),),
),
],
) ,
),
body: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 32),
child: Column(
children: [
Text("We have send verification code on your mobile number",
style: GoogleFonts.oswald(fontStyle: FontStyle.normal,
fontSize: width!*0.0234,
color: HexColor("#8b8b8b")),
),
SizedBox(height: height!/38,),
Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_textFieldOTP(first: true, last: false,controllerr:
contrller1),
_textFieldOTP(first: false, last: false,controllerr:
contrller2),
_textFieldOTP(first: false, last: false,controllerr:
contrller3),
_textFieldOTP(first: false, last: true, controllerr:
contrller4),
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap:() {
resend.phoneNumber= widget.phnNumber;
resend.countryCode = widget.code;
verifyprovider.resendOtp(context,
jsonEncode(resend));
},
child: Text("Resend OTP?",
style: GoogleFonts.oswald(fontStyle:
FontStyle.normal,
fontSize: width!*0.0234,
color: HexColor("#fc4f00")),
),
),
),
SizedBox(height: height!/28,),
GestureDetector(
onTap: (){
if(contrller1!.text.isNotEmpty&&
contrller2!.text.isNotEmpty&&contrller3!.
text.isNotEmpty&&contrller4!.text.isNotEmpty){
verifyOtpRequest verify = verifyOtpRequest();
verify.phoneNumber = widget.phnNumber;
verify.otp=
contrller1!.text+contrller2!.
text+contrller3!.text+contrller4!.text;
verifyprovider.otpVerification(context,
jsonEncode(verify), widget.from);
}else{
CommonUtils.showToast(msg: "Please fill all the
fields ");
}
},
child: Container(
height: height!/18,
width: width,
decoration: BoxDecoration(
color: HexColor("#fc4f00"),
borderRadius: BorderRadius.circular(10)
),
child: Center(
child: Text("Verify",style: TextStyle(
color: Colors.white,
fontSize: width!*0.02345
),),
)
),
),
],
),
],
),
),
),
);
}
Widget _textFieldOTP({bool ? first, last,
TextEditingController ?
controllerr}) {
return Container(
height:height!/12 ,
child: AspectRatio(
aspectRatio: 1.0,
child: TextField(
controller: controllerr,
autofocus: true,
onChanged: (value) {
if (value.length == 1 && last == false) {
FocusScope.of(context).nextFocus();
}
if (value.length == 0 && first == false) {
FocusScope.of(context).previousFocus();
}
},
showCursor: false,
readOnly: false,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
keyboardType: TextInputType.number,
maxLength: 1,
decoration: InputDecoration(
counter: Offstage(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(width: 2, color: Colors.black54),
borderRadius: BorderRadius.circular(12)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(width: 2, color: Colors.black54),
borderRadius: BorderRadius.circular(12)),
),
),
),
);
}
}
When the length of the input data reaches one, you will have to change the text field focus node.
For Example
If you are in the first field, and you enter a number field one focus should be lost, and field two should be in focus. This can be done, by requestFocus.
This article will of help for you: Flutter Focus
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:keyboard_dismisser/keyboard_dismisser.dart';
import 'package:money_formatter/money_formatter.dart';
import 'package:shukela_app/api/banking_app_api.dart';
import 'package:shukela_app/screens/stokvel_detail.dart';
import 'package:shukela_app/screens/stokvels.dart';
import 'package:shukela_app/utils/constants.dart';
import 'package:shukela_app/utils/global_variables.dart';
import 'package:sizer/sizer.dart';
import 'package:shukela_app/model/bankingapp_model.dart';
import '../utils/user_preferences.dart';
class PayMultipleStokvelScreen extends StatefulWidget {
const PayMultipleStokvelScreen({Key? key}) : super(key: key);
#override
State<PayMultipleStokvelScreen> createState() =>
_PayMultipleStokvelScreenState();
}
TextEditingController txtSearch = TextEditingController();
TextEditingController txtAmount = TextEditingController();
class _PayMultipleStokvelScreenState extends State<PayMultipleStokvelScreen> {
String? selectedType;
bool hasText = false;
String buttonText = "PAY NOW";
bool isLoading = false;
Widget? showHideIcon() {
if (hasText) {
return IconButton(
icon: const Icon(
Icons.clear,
color: AppColors.primaryBlue,
),
onPressed: () {
txtSearch.clear();
setState(() {
hasText = false;
});
},
);
} else {
return null;
}
}
// void _showMultiSelectDialog(BuildContext context) async {
// await showDialog(
// context: context,
// builder: (ctx) {
// return MultiSelectDialog(
// items: _animals.map((e) => MultiSelectItem(e, e)).toList(),
// initialValue: _selectedAnimals,
// onConfirm: (values) {...},
// );
// },
// );
// }
double? balance;
final _formKey = GlobalKey<FormState>();
var selectedValue;
List<StokvelDetail> selectedStokvel = [];
#override
void initState() {
super.initState();
balance = double.parse(UserPreferences.getBalance() ?? '');
}
mf() {
MoneyFormatter mf = MoneyFormatter(amount: balance!);
return mf;
}
StokvelListState currentState = StokvelListState.showAllListState;
#override
Widget build(BuildContext context) => KeyboardDismisser(
gestures: const [GestureType.onTap],
child: SafeArea(
child: Scaffold(
backgroundColor: AppColors.secondaryColor,
appBar: AppBar(
backgroundColor: AppColors.secondaryColor,
elevation: 0,
title: const Text('Pay Multiple Stokvel',
style: screenTitleTextStyle),
leading: IconButton(
icon: const Icon(
Icons.arrow_back_ios,
color: AppColors.primaryBlue,
),
onPressed: () => Navigator.pop(context),
),
),
body: Form(
key: _formKey,
child: Column(
children: [
SizedBox(height: 5.h),
Container(
height: 6.h,
width: 98.w,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: TypeAheadFormField<Stokvel?>(
debounceDuration: const Duration(milliseconds: 500),
hideSuggestionsOnKeyboardHide: false,
suggestionsBoxDecoration: const SuggestionsBoxDecoration(
constraints: BoxConstraints(maxHeight: 450),
color: AppColors.secondaryColor,
borderRadius: BorderRadius.all(Radius.circular(10))),
textFieldConfiguration: TextFieldConfiguration(
style: const TextStyle(
color: AppColors.primaryBlue,
fontSize: 15.0,
fontWeight: FontWeight.bold,
),
controller: txtSearch,
onChanged: (value) {
setState(() {
hasText = true;
});
},
decoration: InputDecoration(
prefixIcon: const Icon(
Icons.search,
color: AppColors.primaryBlue,
),
suffixIcon: showHideIcon(),
hintText: 'Search Stokvel',
border: const OutlineInputBorder(
borderSide:
BorderSide(color: AppColors.primaryBlue),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4.0),
borderSide:
const BorderSide(color: AppColors.primaryBlue),
),
),
),
validator: (val) {
if (val!.isEmpty) {
return "Please select stokvel";
}
return null;
},
suggestionsCallback: BankingAppApi.getStokvelSuggestions,
itemBuilder: (context, Stokvel? suggestion) {
final stokvel = suggestion!;
//
return ListTile(
trailing: SizedBox(
height: 20.0,
width: 20.0,
child: Container(
color: AppColors.primaryBlue,
),
),
title: Text(
stokvel.stokvelName!,
style: const TextStyle(
fontFamily: Fonts.primaryFont,
fontWeight: FontWeight.bold,
color: AppColors.primaryBlue,
),
),
subtitle: Text(
stokvel.stokvelType!,
style: const TextStyle(
fontFamily: Fonts.primaryFont,
fontWeight: FontWeight.bold,
color: AppColors.primaryBlue),
),
);
},
noItemsFoundBuilder: (context) => const SizedBox(
height: 60,
child: Center(
child: Text(
'No Stokvel Found.',
style: TextStyle(fontSize: 20),
),
),
),
onSuggestionSelected: (Stokvel? suggestion) {
final stokvel = suggestion!;
setState(() {
txtSearch.text = stokvel.stokvelName!;
hasText = true;
});
stokvelID = stokvel.stokvelID;
memberID = stokvel.memberID;
},
),
),
SizedBox(
height: 4.h,
),
SizedBox(height: 3.h),
Container(
height: 6.h,
width: 98.w,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: TextFormField(
controller: txtAmount,
decoration: const InputDecoration(
labelStyle: TextStyle(
color: AppColors.primaryBlue,
fontSize: 20.0,
fontWeight: FontWeight.bold,
),
suffixText: "ZAR",
border: OutlineInputBorder(
borderSide:
BorderSide(color: AppColors.primaryBlue)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1, color: AppColors.primaryBlue),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1, color: AppColors.primaryBlue),
),
labelText: 'Amount',
contentPadding:
EdgeInsets.only(left: 20, right: 15, bottom: 8),
),
keyboardType: TextInputType.number,
style: const TextStyle(
fontSize: 20.0,
fontFamily: Fonts.primaryFont,
color: AppColors.primaryBlue),
validator: (val) {
if (val!.isEmpty) {
return "Please enter amount";
}
return null;
},
),
),
SizedBox(height: 3.h),
Container(
height: 6.h,
width: 98.w,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: TextField(
readOnly: true,
style: const TextStyle(
fontSize: 18.0, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: "Wallet Balance : R " + mf().output.nonSymbol,
border: const OutlineInputBorder(
borderSide:
BorderSide(color: AppColors.primaryBlue),
borderRadius: BorderRadius.horizontal()),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1, color: AppColors.primaryBlue),
),
enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
borderSide: BorderSide(
width: 1, color: AppColors.primaryBlue),
),
),
),
),
SizedBox(height: 3.h),
Container(
height: 50,
width: 400,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: ElevatedButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Colors.white)),
child: isLoading
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
CircularProgressIndicator(
color: AppColors.secondaryColor),
SizedBox(width: 24),
Text(
"Submitting payment...",
style: TextStyle(
color: AppColors.secondaryColor),
)
],
)
: Text(
buttonText,
style: const TextStyle(
fontFamily: Fonts.primaryFont,
fontWeight: FontWeight.bold,
color: AppColors.primaryBlue),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
if (double.parse(txtAmount.text) <= balance!) {
setState(
() {
isLoading = true;
},
);
stokvelTransact.amount =
double.parse(txtAmount.text);
stokvelTransact.memberID = memberID;
stokvelTransact.stokvelID = stokvelID;
stokvelTransact.transactionTypeID = 1;
api
.stokvelDeposit(stokvelTransact,
"StokvelTransaction/StokvelTransact")
.then(
(value) => setState(
() {
Future.delayed(
const Duration(seconds: 3));
isLoading = false;
if (value == "Success") {
ScaffoldMessenger.of(context)
.showSnackBar(snackBar(
content:
'Payment made succesfully',
duration: 5));
} else {
ScaffoldMessenger.of(context)
.showSnackBar(snackBar(
content:
'We have encountered technical problems, Try again later',
duration: 5));
}
},
),
)
.catchError(
(err) {
setState(() {
isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
snackBar(
content: err.toString(), duration: 7));
},
);
} else {
ScaffoldMessenger.of(context).showSnackBar(snackBar(
content: "Insuficient funds!", duration: 7));
}
}
},
),
),
],
),
),
),
),
);
}
The above code is for the single page that I want to achieve this
This is what I want to achieve or any suggestions are accepted, I want to mark the Group of Stokvels I want to pay and pay them different accounts I don't know if it's possible please I'm new in flutter it a project I have to get it done as soon as possible this is the source code for my whole payment page, for now, I'm able to pay a single Stokvel Group but I want to select multiple
I highly suggest starting with formatting of the current code - its hard to read and with that hard to maintain.
The simple idea would be:
use List<StokvelDetail> selectedStokvel = []; you created to add the Stokvel's in every time you select Stokvel in SearchBar
Add Map<int,double> amounts = {}; <StokvelId, amount> to keep track of the amount you want to send to each Stokvel
Based on selectedStokvel build multiple 'amount window' widgets
Before submit verify walletBalance >= amounts.values.reduce((a,b)=> a + b)
For each selectedStokvel call api.stokvelDeposit(stokvelTransact, ...
I would primarly focus on spliting this code to multiple Widget classes, there are many schools but I like to have around 150 lines of code in single file at most.
From what i see your file can be easily split into:
Column(
children: [
SizedBox(),
SearchBar(),
SizedBox(),
AmountInput(), --> StokvelAmountInput() for each selectedStokvel
SizedBox(),
WalletBalance(),
SizedBox(),
SubmitButton(),
]),
```