I am working on a Flutter Application, How do i select a single toggle button at a time, if i have multiple toggle buttons?
The problem is i have multiple cases with multiple choices within each case, i have 5 different cases.
onPressed: (int index) {
setState(() {
isSelected2[index] = !isSelected2[index];
switch (index) {
//This is the other area I had to make changes
case 0:
if (isSelected2[index]) {
print('true');
_choiceA += 5;
_choiceB += 5;
_choiceC += 10;
_choiceD += 10;
} else {
print('false');
_choiceA += -5;
_choiceB += -5;
_choiceC += -10;
_choiceD += -10;
}
break;
Thank you
Mohammad
Checkout the code below:
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: SamplePage(),
);
}
}
class SamplePage extends StatefulWidget {
#override
_SamplePageState createState() => _SamplePageState();
}
class _SamplePageState extends State<SamplePage> {
List<bool> isSelected;
#override
void initState() {
// this is for 3 buttons, add "false" same as the number of buttons here
isSelected = [true, false, false];
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ToggleButtons Demo'),
),
body: Center(
child: ToggleButtons(
children: <Widget>[
// first toggle button
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'First',
),
),
// second toggle button
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Second',
),
),
// third toggle button
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Third',
),
),
],
// logic for button selection below
onPressed: (int index) {
setState(() {
for (int i = 0; i < isSelected.length; i++) {
isSelected[i] = i == index;
}
});
},
isSelected: isSelected,
),
),
);
}
}
Output:
You need to disable all others to achieve desirable behavior.
Make a list of values you want to update for each button index:
List values = [[5,5,10,10], [3,2,3,-5], [3,-1,3,4], [3,-8, 12,1]];
Then update your choices:
onPressed: (int index) {
setState(() {
for (int buttonIndex = 0; buttonIndex < isSelected.length; buttonIndex++) {
if (buttonIndex == index) {
isSelected[buttonIndex] = !isSelected[buttonIndex];
if(isSelected[buttonIndex]){
_choiceA += values[buttonIndex][0];
_choiceB += values[buttonIndex][1];
_choiceC += values[buttonIndex][2];
_choiceD += values[buttonIndex][3];
}else{
_choiceA -= values[buttonIndex][0];
_choiceB -= values[buttonIndex][1];
_choiceC -= values[buttonIndex][2];
_choiceD -= values[buttonIndex][3];
isSelected[buttonIndex] = false;
}
} else {
if(isSelected[buttonIndex]){
_choiceA -= values[buttonIndex][0];
_choiceB -= values[buttonIndex][1];
_choiceC -= values[buttonIndex][2];
_choiceD -= values[buttonIndex][3];
isSelected[buttonIndex] = false;
}
}
}
});
},
Edit:
This code should be refactored
Related
I want to create a list o buttons with text so that the user can select one of them.
For the state of the buttons I used a StateNotifier:
class SeleccionStateNotifier extends StateNotifier<List<bool>> {
int cantidad;
SeleccionStateNotifier(this.cantidad)
: super(List.generate(cantidad, (index) => false));
void CambioValor(int indice) {
for (int i = 0; i < cantidad; i++) {
if (i == indice) {
state[i] = true;
} else {
state[i] = false;
}
}
}
}
final seleccionProvider =
StateNotifierProvider<SeleccionStateNotifier, List<bool>>((ref) {
final lector = ref.watch(eleccionesSinSeleccionStateNotifierProvider);
return SeleccionStateNotifier(lector.length);
});
Now in the UI I just want to show a text and the value of the button (false for everyone except the one the user selects)
class EleccionesList5 extends ConsumerWidget {
const EleccionesList5({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, ScopedReader watch) {
bool este = false;
final eleccioneswatchCon =
watch(eleccionesSinSeleccionStateNotifierProvider);
final seleccionwatch = watch(seleccionProvider);
final buttons = List<Widget>.generate(
eleccioneswatchCon.length,
(i) => Container(
padding: const EdgeInsets.fromLTRB(5, 2, 5, 2),
child: TextButton(
onPressed: () {
context.read(seleccionProvider.notifier).CambioValor(i);
print('OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO');
for (int id = 0; id < eleccioneswatchCon.length; id++) {
print(seleccionwatch[id]);
}
},
child: Text(eleccioneswatchCon[i].eleccion + ' ' + seleccionwatch[i].toString()),
),
),
).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Wrap(
alignment: WrapAlignment.spaceAround,
children: buttons,
),
],
);
}
}
based on my previous answer you should update state itself, not an inner value of a list to take effect
void CambioValor(int indice) {
final list = List.of(state);
for (int i = 0; i < cantidad; i++) {
if (i == indice) {
list[i] = true;
} else {
list[i] = false;
}
}
/// it will update when you use the setter of state
state = list;
}
I have been writing a Sorting visualiser in flutter, I am so far able to animate the movement of blocks. But I also want to update the colours of the block, when the block goes through the states of being scanned, moved, and finally when it is completely sorted. I looked up the State management in flutter, and it is rather confusing to know what approach should I be using in my project. Below is the DashBoard Class:
import 'package:algolizer/sortingAlgorithms/Block.dart';
import 'package:flutter/material.dart';
import 'dart:math';
class DashBoard extends StatefulWidget {
double width;
double height;
DashBoard(#required this.width, #required this.height);
#override
_DashBoardState createState() => _DashBoardState();
}
class _DashBoardState extends State<DashBoard> {
double currentSliderValue = 50;
List<double> arr = new List(500);
List<Block> blockList;
bool running = false;
#override
void initState() {
// TODO: implement initState
super.initState();
fillArr((widget.width * 0.6) / 50, (widget.width * 0.1) / 50,
widget.height * 0.7);
}
void InsertionSort() async {
setState(() {
running = true;
});
int delay = (pow(15, 4) / pow(currentSliderValue, 2)).round();
for (int i = 1; i < currentSliderValue; i++) {
if (blockList[i] == null) break;
Block key = blockList[i];
int j = i - 1;
while (j >= 0 && blockList[j].height > key.height) {
setState(() {
blockList[j + 1] = blockList[j];
});
await Future.delayed(Duration(milliseconds: delay));
j--;
}
blockList[j + 1] = key;
}
setState(() {
running = false;
});
}
void BubbleSort() async {
setState(() {
running = true;
});
int delay = (pow(15, 4) / pow(currentSliderValue, 2)).round();
for (int i = 0; i < currentSliderValue - 1; i++) {
for (int j = 0; j < currentSliderValue - i - 1; j++) {
if (blockList[j].height > blockList[j + 1].height) {
Block temp = blockList[j + 1];
setState(() {
blockList[j + 1] = blockList[j];
blockList[j] = temp;
});
await Future.delayed(Duration(milliseconds: delay));
}
}
}
setState(() {
running = false;
});
}
// Map<String, >
void fillArr(double width, double margin, double height) {
for (int i = 0; i < arr.length; i++) arr[i] = null;
var rng = new Random();
for (int i = 0; i < currentSliderValue; i++) {
double val = rng.nextDouble() * height;
if (val == 0)
continue;
else
arr[i] = val;
}
blockList = [...arr.map((height) => Block(height, width, margin))];
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 20),
Row(
children: [
Text(
"Length",
),
Slider(
value: currentSliderValue,
min: 5,
max: 200,
onChanged: (double value) {
setState(() {
currentSliderValue = value;
});
double newwidth =
(MediaQuery.of(context).size.width * 0.6) /
currentSliderValue;
double newmargin =
(MediaQuery.of(context).size.width * 0.1) /
currentSliderValue;
fillArr(newwidth, newmargin, widget.height * 0.7);
}),
RaisedButton(
child: Text("Insertion Sort"),
onPressed: InsertionSort,
),
RaisedButton(
onPressed: BubbleSort, child: Text("Bubble Sort")),
RaisedButton(onPressed: () {}, child: Text("Merge Sort")),
RaisedButton(onPressed: () {}, child: Text("Quick Sort")),
RaisedButton(
onPressed: () {}, child: Text("Counting Sort")),
RaisedButton(onPressed: () {}, child: Text("Radix Sort")),
RaisedButton(
onPressed: () {}, child: Text("Selection Sort")),
RaisedButton(onPressed: () {}, child: Text("Heap Sort")),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [...blockList],
),
// Row(
// children: [
// Container(
// child: Row(children: [
// Text("Algorithm")
// ],)
// )]
// ),
],
),
),
);
}
}
Here's the Block class:
import 'package:flutter/material.dart';
class Block extends StatefulWidget {
Block(#required this.height, #required this.width, #required this.mar);
double height;
double width;
double mar;
#override
_BlockState createState() => _BlockState();
}
class _BlockState extends State<Block> {
Color col = Colors.blue;
// void isKey() {
// setState(() {
// col = Colors.pink;
// });
// }
// void notKey() {
// setState(() {
// col = Colors.purple;
// });
// }
#override
Widget build(BuildContext context) {
return (widget.height == null)
? Container()
: Container(
height: this.widget.height,
width: widget.width,
margin: EdgeInsets.all(widget.mar),
decoration: BoxDecoration(
color: col,
),
);
}
}
As far as which state management route to go with, it really can be done with any of them. GetX to me is the easiest and has the least boilerplate.
Here's one way to do this. I just updated the insertionSort method to get you started and you can go from there. Any other changes you notice in your other classes are just to get rid of linter errors.
All your methods and variables can now live in a GetX class. With the exception of color, the rest are now observable streams.
class BlockController extends GetxController {
RxDouble currentSliderValue = 50.0.obs; // adding .obs makes variable obserable
RxList arr = List(500).obs;
RxList blockList = [].obs;
RxBool running = false.obs;
Color color = Colors.red;
void insertionSort() async {
running.value = true; // adding .value access the value of observable variable
color = Colors.blue;
int delay = (pow(15, 4) / pow(currentSliderValue.value, 2)).round();
for (int i = 1; i < currentSliderValue.value; i++) {
if (blockList[i] == null) break;
Block key = blockList[i];
int j = i - 1;
while (j >= 0 && blockList[j].height > key.height) {
blockList[j + 1] = blockList[j];
await Future.delayed(Duration(milliseconds: delay));
j--;
}
blockList[j + 1] = key;
}
color = Colors.green;
update(); // only needed for the color property because its not an observable stream
running.value = false;
}
void bubbleSort() async {
running.value = true;
int delay = (pow(15, 4) / pow(currentSliderValue.value, 2)).round();
for (int i = 0; i < currentSliderValue.value - 1; i++) {
for (int j = 0; j < currentSliderValue.value - i - 1; j++) {
if (blockList[j].height > blockList[j + 1].height) {
Block temp = blockList[j + 1];
blockList[j + 1] = blockList[j];
blockList[j] = temp;
await Future.delayed(Duration(milliseconds: delay));
}
}
}
running.value = false;
}
// Map<String, >
void fillArr(double width, double margin, double height) {
for (int i = 0; i < arr.length; i++) arr[i] = null;
var rng = new Random();
for (int i = 0; i < currentSliderValue.value; i++) {
double val = rng.nextDouble() * height;
if (val == 0)
continue;
else
arr[i] = val;
}
blockList = [...arr.map((height) => Block(height, width, margin))].obs;
}
}
Initialize the controller in your main before running your app. Generally it can be done anywhere as long as its before you try to access the controller.
Get.put(BlockController());
Here's your much less busy DashBoard now that all that logic is tucked away in a GetX class. Here we find the controller, and use it access all those variables and methods.
Obx is the GetX widget that rebuilds on changes.
class DashBoard extends StatefulWidget {
final double width;
final double height;
DashBoard(this.width, this.height);
#override
_DashBoardState createState() => _DashBoardState();
}
class _DashBoardState extends State<DashBoard> {
final controller = Get.find<BlockController>(); // finding same instance of BlockConroller that you initialized in `Main`
#override
void initState() {
super.initState();
controller.fillArr((widget.width * 0.6) / 50, (widget.width * 0.1) / 50,
widget.height * 0.7);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 50),
Obx(
// rebuilds when observable variables change
() => Column(
// changed to Column because a Row was overflowing
children: [
Text(
"Length",
),
Slider(
value: controller.currentSliderValue.value,
min: 5,
max: 200,
onChanged: (double value) {
controller.currentSliderValue.value = value;
double newwidth =
(MediaQuery.of(context).size.width * 0.6) /
controller.currentSliderValue.value;
double newmargin =
(MediaQuery.of(context).size.width * 0.1) /
controller.currentSliderValue.value;
controller.fillArr(
newwidth, newmargin, widget.height * 0.7);
}),
RaisedButton(
child: Text("Insertion Sort"),
onPressed: controller.insertionSort,
),
RaisedButton(
onPressed: controller.bubbleSort,
child: Text("Bubble Sort")),
RaisedButton(onPressed: () {}, child: Text("Merge Sort")),
RaisedButton(onPressed: () {}, child: Text("Quick Sort")),
RaisedButton(onPressed: () {}, child: Text("Counting Sort")),
RaisedButton(onPressed: () {}, child: Text("Radix Sort")),
RaisedButton(onPressed: () {}, child: Text("Selection Sort")),
RaisedButton(onPressed: () {}, child: Text("Heap Sort")),
],
),
),
Obx(
// rebuilds when observable variables change
() => Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [...controller.blockList],
),
),
// Row(
// children: [
// Container(
// child: Row(children: [
// Text("Algorithm")
// ],)
// )]
// ),
],
),
),
);
}
}
And here's your Block which can now be stateless. Key thing of note here is the GetBuilder widget that updates the color.
class Block extends StatelessWidget {
// now can be stateless
Block(this.height, this.width, this.mar);
final double height;
final double width;
final double mar;
#override
Widget build(BuildContext context) {
return (height == null)
? Container()
: GetBuilder<BlockController>(
// triggers rebuilds when update() is called from GetX class
builder: (controller) => Container(
height: this.height,
width: width,
margin: EdgeInsets.all(mar),
decoration: BoxDecoration(
color: controller.color,
),
),
);
}
}
I have an AnimatedList whose initial items are getting added before loading data from SharedPreferences. Is there a way, I can load data from device first and then build the screen.
Let's say I'm loading data into myList by for loop using SharedPreferences. And then using the data from myList to create AnimatedList. The problem I'm facing is, the data that was loaded is not Showing in AnimatedList. AnimatedList is getting build before loading the data.
import 'package:material_todo/index.dart';
class TaskScreen extends StatefulWidget {
TaskScreen({this.taskProvider});
final TaskProvider taskProvider;
#override
_TaskScreenState createState() => _TaskScreenState();
}
class _TaskScreenState extends State<TaskScreen>
with SingleTickerProviderStateMixin {
#override
void initState() {
super.initState();
widget.taskProvider.initialiseController(this);
}
String taskRemaining(List tasks) {
var remain = 0;
for (var i in tasks) {
if (i[2] == false) {
remain += 1;
}
}
return '$remain';
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: kWhiteColor,
appBar: widget.taskProvider.editTask
? AppBar(
backgroundColor: Colors.black54,
title: Text('Edit mode'),
centerTitle: false,
leading: IconButton(
onPressed: () {
widget.taskProvider.selectTask(false);
widget.taskProvider.toggleAddTask(false);
widget.taskProvider.toggleEditTask(false);
},
icon: Icon(
Icons.close,
color: Colors.white,
),
),
actions: <Widget>[
IconButton(
onPressed: () {
widget.taskProvider.editingTask();
},
icon: Icon(
Icons.edit,
color: Colors.white,
),
),
IconButton(
onPressed: () {
widget.taskProvider.deleteTask();
widget.taskProvider.toggleEditTask(false);
widget.taskProvider.toggleAddTask(false);
},
icon: Icon(
Icons.delete,
color: Colors.white,
),
),
],
)
: AppBar(
title: Text(
'Tasks',
),
actions: <Widget>[
IconButton(
onPressed: () {
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) =>
// SettingsScreen(provider: taskProvider),
// ),
// );
widget.taskProvider.clearData();
},
icon: Icon(
Icons.settings,
color: Colors.white,
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (widget.taskProvider.showAddTask) {
widget.taskProvider.toggleAddTask(false);
} else {
widget.taskProvider.toggleAddTask(true);
}
try {
widget.taskProvider.selectTask(false);
widget.taskProvider.toggleEditTask(false);
} catch (e) {
print(e);
}
},
child: RotationTransition(
turns: Tween(begin: 0.0, end: 0.125)
.animate(widget.taskProvider.rotationalController),
child: Icon(
Icons.add,
),
),
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
widget.taskProvider.showAddTask
? BuildTaskInput(widget.taskProvider)
: Container(
height: 0,
width: 0,
),
Padding(
padding:
const EdgeInsets.only(left: 30.0, top: 30.0, bottom: 20.0),
child: Text(
taskRemaining(widget.taskProvider.tasks) + ' tasks remaining',
style: TextStyle(
fontSize: 20.0,
color: Colors.grey[800],
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: AnimatedList(
key: widget.taskProvider.listKey,
initialItemCount: widget.taskProvider.tasks.length,
itemBuilder: (context, index, animation) {
return SizeTransition(
sizeFactor: animation,
child: TaskTile(
title: widget.taskProvider.tasks[index][0],
due: widget.taskProvider.tasks[index][1],
isChecked: widget.taskProvider.tasks[index][2],
isSelected: widget.taskProvider.tasks[index][3],
changeValue: (newValue) {
widget.taskProvider.checkTask(newValue, index);
},
editTask: () {
widget.taskProvider.setTileIndex(index);
widget.taskProvider.selectTask(true);
widget.taskProvider.toggleEditTask(true);
},
),
);
}),
),
],
),
),
);
}
}
import 'package:material_todo/index.dart';
class TaskProvider extends ChangeNotifier {
TaskProvider() {
loadData();
}
//Properties
TextEditingController _textController = TextEditingController();
AnimationController _rotationController;
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
int _tileIndex = 0; //index of selected tile.
bool _showAddTask = false; //whether to show add task container.
bool _editTask = false; // edit task.
bool _deleteCheckTasks = false;
bool _bottomCheckTasks = true;
List<List> _tasks = [
['abc', 'Today', false, false]
]; //stores all the task data.
//Getters
List<List> get tasks => _tasks;
bool get showAddTask => _showAddTask;
bool get editTask => _editTask;
bool get deleteCheckTasks => _deleteCheckTasks;
bool get bottomCheckTasks => _bottomCheckTasks;
int get tileIndex => _tileIndex;
TextEditingController get textController => _textController;
AnimationController get rotationalController => _rotationController;
GlobalKey<AnimatedListState> get listKey => _listKey;
//Setters
void setTileIndex(int index) {
_tileIndex = index;
notifyListeners();
}
void addTask(String title, String due) {
int index = addTaskAtProperIndex(due);
_tasks.insert(index, [
title,
due,
false,
false,
]);
_listKey.currentState.insertItem(index);
notifyListeners();
saveData();
}
void toggleAddTask(bool value) {
_showAddTask = value;
if (!value) {
_rotationController.reverse();
_textController.clear();
} else {
_rotationController.forward();
}
notifyListeners();
print(value);
}
void toggleEditTask(bool value) {
_editTask = value;
notifyListeners();
}
void toggleDeleteTask(bool value) {
_deleteCheckTasks = value;
if (value) {
for (int i = _tasks.length - 1; i >= 0; i--) {
if (_tasks[i][2] == true) {
_tasks.removeAt(i);
}
}
}
notifyListeners();
saveData();
}
void toggleBottomCheckTask(bool value) {
_bottomCheckTasks = value;
if (value) {
for (int i = _tasks.length - 1; i >= 0; i--) {
if (_tasks[i][2] == true) {
_tasks.add(_tasks[i]);
_tasks.removeAt(i);
}
}
}
notifyListeners();
saveData();
}
void checkTask(bool value, int index) {
_tasks[index][2] = value;
if (value && _deleteCheckTasks) {
deleteTask();
}
if (value && _bottomCheckTasks) {
_tasks.add(_tasks[index]);
_tasks.removeAt(index);
}
notifyListeners();
saveData();
}
void selectTask(bool value) {
_tasks[_tileIndex][3] = value;
notifyListeners();
saveData();
}
void deleteTask() {
_tasks.removeAt(_tileIndex);
notifyListeners();
saveData();
}
void replaceTask(String title, String due) {
_tasks.removeAt(_tileIndex);
addTask(title, due);
selectTask(false);
toggleEditTask(false);
notifyListeners();
saveData();
}
void editingTask() {
toggleAddTask(true);
_textController.value = TextEditingValue(text: _tasks[_tileIndex][0]);
}
int addTaskAtProperIndex(String due) {
for (int i = 0; i < _tasks.length; i++) {
if (_tasks[i][1] == due) {
return i;
}
if (due == 'Today') {
return 0;
}
if (due == 'Tomorrow') {
for (int i = _tasks.length - 1; i >= 0; i--) {
if (_tasks[i][1] == 'Today') {
return i + 1;
}
}
return 0;
}
if (due == 'Anytime') {
for (int i = _tasks.length - 1; i >= 0; i--) {
if (_tasks[i][1] == 'Tomorrow') {
return i + 1;
}
}
for (int i = _tasks.length - 1; i >= 0; i--) {
if (_tasks[i][1] == 'Today') {
return i + 1;
}
}
return 0;
}
}
return 0;
}
void initialiseController(TickerProvider ticker) {
_rotationController = AnimationController(
duration: const Duration(milliseconds: 200), vsync: ticker);
}
void saveData() async {
SharedPreferences _prefs = await SharedPreferences.getInstance();
_prefs.setInt('count', _tasks.length);
_prefs.setBool('deleteCheckTasks', _deleteCheckTasks);
_prefs.setBool('bottomCheckTasks', _bottomCheckTasks);
for (int i = 0; i < _tasks.length; i++) {
_prefs.setString('$i 0', _tasks[i][0]);
_prefs.setString('$i 1', _tasks[i][1]);
_prefs.setBool('$i 2', _tasks[i][2]);
}
print('data saved successfully');
}
Future loadData() async {
SharedPreferences _prefs = await SharedPreferences.getInstance();
var _count = _prefs.getInt('count');
if (_count != null) {
for (int i = 0; i < _count; i++) {
var _title = _prefs.getString('$i 0');
var _due = _prefs.getString('$i 1');
var _check = _prefs.getBool('$i 2');
_tasks.add([_title, _due, _check, false]);
}
print('dada loaded successfully');
} else {
print('No data was found');
}
if (_prefs.getBool('deleteCheckTasks') != null) {
_deleteCheckTasks = _prefs.getBool('deleteCheckTasks');
}
if (_prefs.getBool('bottomCheckTasks') != null) {
_bottomCheckTasks = _prefs.getBool('bottomCheckTasks');
}
notifyListeners();
}
Future clearData() async {
SharedPreferences _prefs = await SharedPreferences.getInstance();
_prefs.clear();
}
}
I recently wrote a test program that I needed that is essentially a CRUD program. I needed to handle this differently to other similar programs that I have written, because I normally use a stateful FAB widget, and don't have to setState() to enable and disable the FAB. In this test program I didn't want to use the custom FAB, and used the standard FAB. I found that whenever I had to enable or disable the FAB because of a change to a TextField, that this required a setState(), and after the build, the cursor for the TextField that was being edited had repositioned. I don't know why that happens, because I had not recreated the Widgets. The only solution that I could come up with to handle that issue was fairly messy and required saving the Widget position in the List of TextField and also save the Selection, and then after the build resetting the Selection to the saved Selection.
What I need to achieve is for the FAB to be only enabled when data has changed. Obviously this can vary with every key entry.
I presume I'm not handling this in the optimal way. How is this handled so that the cursor position remains as it was prior to the build?
----- Have Now Added Code below ----
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(MyApp());
//=====================================================================================
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test Widgets',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(title: 'Test Widgets'),
);
}
}
//=====================================================================================
class HomePage extends StatefulWidget {
HomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_HomePageState createState() => _HomePageState();
}
//=====================================================================================
class _HomePageState extends State<HomePage> {
bool _tfDataHasChanged = false;
bool _tfInitialized = false;
bool _tfSaveSelection = false;
int _iNdxWidgetChanged = -1;
List<String> _lsOldData = ['Row 1', 'Row 2', 'Row 3', 'Row 4'];
List<String> _lsNewData = ['Row 1', 'Row 2', 'Row 3', 'Row 4'];
List<TextField> _lwTextFields;
TextSelection _wTextSelection;
//-------------------------------------------------------------------------------------
#override
void dispose() {
for (int iNdxWidget = 0;
_lwTextFields != null && iNdxWidget < _lwTextFields.length;
iNdxWidget++) {
_lwTextFields[iNdxWidget].focusNode.removeListener(() {
_fnFocusChanged();
});
_lwTextFields[iNdxWidget]?.controller?.dispose();
_lwTextFields[iNdxWidget]?.focusNode?.dispose();
}
super.dispose();
}
//-------------------------------------------------------------------------------------
#override
Widget build(BuildContext context) {
_tfInitialized = false;
SchedulerBinding.instance.addPostFrameCallback((_) => _fnOnBuildComplete());
if (_lwTextFields == null) {
_fnCreateAllWidgets();
}
List<Widget> lwDisplay = _fnCreateDisplay();
return Scaffold(
appBar: AppBar(
flexibleSpace: SafeArea(
child: _fnCreateAppBarWidgets(),
)),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: lwDisplay,
),
),
floatingActionButton: FloatingActionButton(
onPressed: _tfDataHasChanged ? _fnUpdateData : null,
tooltip: 'Update',
backgroundColor: _tfDataHasChanged ? Colors.blue : Colors.grey,
child: Icon(Icons.done),
),
);
}
//-------------------------------------------------------------------------------------
_fnOnBuildComplete() {
_tfInitialized = true;
if (_tfSaveSelection && _iNdxWidgetChanged >= 0) {
_lwTextFields[_iNdxWidgetChanged].controller.selection = _wTextSelection;
}
}
//-------------------------------------------------------------------------------------
void _fnCreateAllWidgets() {
_lwTextFields = List(_lsNewData.length);
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
_fnCreateTextField(iNdxWidget);
}
}
//-------------------------------------------------------------------------------------
void _fnCreateTextField(int iNdxWidget) {
TextEditingController wController = TextEditingController();
FocusNode wFocusNode = FocusNode();
wFocusNode.addListener(() => _fnFocusChanged());
_lwTextFields[iNdxWidget] = TextField(
autofocus: false, //(iNdxWidget == 0),
autocorrect: false,
enabled: true,
keyboardType: TextInputType.text,
maxLength: 25,
controller: wController,
focusNode: wFocusNode,
textInputAction: TextInputAction.next /* TYPE OF ACTION KEY */,
onSubmitted: ((v) => _fnSetNextFocus(iNdxWidget)),
onChanged: (text) => _fnTextListener(iNdxWidget, text),
decoration: _fnCreateInputDecoration(
'Text Field Number ${iNdxWidget + 1}', 'Enter Data'),
style: _fnCreateWidgetTextStyle(Colors.blue[700]),
);
}
//-------------------------------------------------------------------------------------
_fnTextListener(int iNdxWidget, String sText) {
if (_tfInitialized) {
_lsNewData[iNdxWidget] = sText;
_fnCheckIfDataHasChanged(
iNdxWidget) /* ENABLE OR DISABLE SUBMIT BUTTON */;
}
}
//-------------------------------------------------------------------------------------
_fnSetNextFocus(int iNdxWidget) {
if (_lwTextFields[iNdxWidget].focusNode.hasFocus) {
_lwTextFields[iNdxWidget].focusNode.unfocus();
if (iNdxWidget + 1 < _lwTextFields.length) {
_lwTextFields[iNdxWidget + 1]?.focusNode?.requestFocus();
}
}
}
//-------------------------------------------------------------------------------------
InputDecoration _fnCreateInputDecoration(String sHeading, String sHint) {
return InputDecoration(
labelText: sHeading,
hintText: sHint,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(20.0)),
);
}
//-------------------------------------------------------------------------------------
TextStyle _fnCreateWidgetTextStyle(Color color) {
return TextStyle(
fontSize: 14.0,
color: color,
);
}
//-------------------------------------------------------------------------------------
List<Widget> _fnCreateDisplay() {
List<Widget> lwDisplay = List((_lwTextFields.length * 2) + 2);
lwDisplay[0] = SizedBox(height: 10);
int iNdxDisplay = 1;
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
_lwTextFields[iNdxWidget].controller.text = _lsNewData[iNdxWidget];
lwDisplay[iNdxDisplay++] = _lwTextFields[iNdxWidget];
lwDisplay[iNdxDisplay++] =
SizedBox(height: iNdxDisplay < lwDisplay.length - 2 ? 10 : 80);
}
lwDisplay[lwDisplay.length - 1] = Divider(color: Colors.black, height: 2);
return lwDisplay;
}
//-------------------------------------------------------------------------------------
_fnUpdateData() {
for (int iNdxWidget = 0; iNdxWidget < _lsNewData.length; iNdxWidget++) {
if (_lsNewData[iNdxWidget] != _lsOldData[iNdxWidget]) {
_lsOldData[iNdxWidget] = _lsNewData[iNdxWidget];
}
}
_fnCheckIfDataHasChanged(-1);
}
//-------------------------------------------------------------------------------------
_fnCheckIfDataHasChanged(int iNdxWidgetChanged) {
bool tfChanged = false /* INIT */;
for (int iNdxWidgetTest = 0;
!tfChanged && iNdxWidgetTest < _lsNewData.length;
iNdxWidgetTest++) {
tfChanged = _lsNewData[iNdxWidgetTest] != _lsOldData[iNdxWidgetTest];
}
if (iNdxWidgetChanged >= 0) {
_iNdxWidgetChanged = iNdxWidgetChanged;
_wTextSelection = _lwTextFields[iNdxWidgetChanged].controller.selection;
}
if (tfChanged != _tfDataHasChanged) {
setState(() => _tfDataHasChanged = tfChanged) /* WE NEED TO ENABLE FAB */;
}
}
//-------------------------------------------------------------------------------------
Row _fnCreateAppBarWidgets() {
IconData wIconData =
_tfSaveSelection ? Icons.check_box : Icons.check_box_outline_blank;
Color wColor = _tfSaveSelection ? Colors.blue[900] : Colors.grey[600];
IconButton wIconButton = IconButton(
icon: Icon(wIconData),
color: wColor,
onPressed: _fnCheckboxChanged,
iconSize: 40);
return Row(children: <Widget>[
SizedBox(width: 10),
Text('Save\nSelection', textAlign: TextAlign.center),
wIconButton,
SizedBox(width: 30),
Text('Test TextField')
]);
}
//-------------------------------------------------------------------------------------
_fnFocusChanged() {
for (int iNdxWidget = 0; iNdxWidget < _lwTextFields.length; iNdxWidget++) {
if (_lwTextFields[iNdxWidget].focusNode.hasFocus) {
_iNdxWidgetChanged = iNdxWidget;
_wTextSelection = _lwTextFields[iNdxWidget].controller.selection;
return;
}
}
}
//-------------------------------------------------------------------------------------
void _fnCheckboxChanged() {
_tfSaveSelection = !_tfSaveSelection;
if (!_tfSaveSelection) {
_iNdxWidgetChanged = -1;
}
setState(() {});
}
}
-------- Have added key to TextField, but issue persists ---------
key: ValueKey<int>(iNdxWidget),
My bug - as posted by #pskink
My excuse - I normally use a stateful FAB, so I don't normally encounter this.
Answer:
so change this line:
TextEditingController wController = TextEditingController(text: _lsNewData[iNdxWidget]);
and remove this one
_lwTextFields[iNdxWidget].controller.text = _lsNewData[iNdxWidget];
– pskink Feb 23 at 7:33
I hope these functions might help you
void updateText(String text) {
if (text != null) {
this.text = _applyMask(mask, text);
} else {
this.text = '';
}
_lastUpdatedText = this.text;
}
void updateMask(String mask, {bool moveCursorToEnd = true}) {
this.mask = mask;
updateText(text);
if (moveCursorToEnd) {
this.moveCursorToEnd();
}
}
void moveCursorToEnd() {
final String text = _lastUpdatedText;
selection =
TextSelection.fromPosition(TextPosition(offset: (text ?? '').length));
}
I'm looking to recreate Snapchat's back-to-back video format in Flutter. Since video_player is lacking callbacks for when the video finishes (and is otherwise prone to callback hell), I was wondering if anyone has some pointers for building something like this.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
void main() {
runApp(MaterialApp(
title: 'My app', // used by the OS task switcher
home: MyHomePage(),
));
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<VideoPlayerController> _controllers = [];
VoidCallback listener;
bool _isPlaying = false;
int _current = 0;
#override
void initState() {
super.initState();
// Add some sample videos
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
_controllers.add(VideoPlayerController.network(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
));
this.tick();
// Try refreshing by brute force (this isn't going too well)
new Timer.periodic(Duration(milliseconds: 100), (Timer t) {
int delta = 99999999;
if(_controllers[_current].value != null) {
delta = (_controllers[_current].value.duration.inMilliseconds - _controllers[_current].value.position.inMilliseconds);
}
print("Tick " + delta.toString());
if(delta < 500) {
_current += 1;
this.tick();
}
});
}
void tick() async {
print("Current: " + _current.toString());
await _controllers[_current].initialize();
await _controllers[_current].play();
print("Ready");
setState((){
_current = _current;
});
}
#override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: _controllers[_current].value.aspectRatio,
// Use the VideoPlayer widget to display the video
child: VideoPlayer(_controllers[_current]),
);
}
}
What I have now plays the first video, but there is a very long delay between the first and second. I believe it has to do with my inability to get rid of the listener attached to the 0th item.
Initializing a network VideoPlayerController may take some time to finish. You can initialize the controller of the next video while playing the current. This will take more memory but I don't think it will create huge problems if you prebuffer only one or two videos. Then when the next or previous buttons get pressed, video will be ready to play.
Here is my workaround. It's functional, it prebuffers previous and next videos, skips to the next video when finishes, shows the current position and buffer, pauses and plays on long press.
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
main() {
runApp(MaterialApp(
home: VideoPlayerDemo(),
));
}
class VideoPlayerDemo extends StatefulWidget {
#override
_VideoPlayerDemoState createState() => _VideoPlayerDemoState();
}
class _VideoPlayerDemoState extends State<VideoPlayerDemo> {
int index = 0;
double _position = 0;
double _buffer = 0;
bool _lock = true;
Map<String, VideoPlayerController> _controllers = {};
Map<int, VoidCallback> _listeners = {};
Set<String> _urls = {
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#1',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#2',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#3',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#4',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#5',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#6',
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4#7',
};
#override
void initState() {
super.initState();
if (_urls.length > 0) {
_initController(0).then((_) {
_playController(0);
});
}
if (_urls.length > 1) {
_initController(1).whenComplete(() => _lock = false);
}
}
VoidCallback _listenerSpawner(index) {
return () {
int dur = _controller(index).value.duration.inMilliseconds;
int pos = _controller(index).value.position.inMilliseconds;
int buf = _controller(index).value.buffered.last.end.inMilliseconds;
setState(() {
if (dur <= pos) {
_position = 0;
return;
}
_position = pos / dur;
_buffer = buf / dur;
});
if (dur - pos < 1) {
if (index < _urls.length - 1) {
_nextVideo();
}
}
};
}
VideoPlayerController _controller(int index) {
return _controllers[_urls.elementAt(index)];
}
Future<void> _initController(int index) async {
var controller = VideoPlayerController.network(_urls.elementAt(index));
_controllers[_urls.elementAt(index)] = controller;
await controller.initialize();
}
void _removeController(int index) {
_controller(index).dispose();
_controllers.remove(_urls.elementAt(index));
_listeners.remove(index);
}
void _stopController(int index) {
_controller(index).removeListener(_listeners[index]);
_controller(index).pause();
_controller(index).seekTo(Duration(milliseconds: 0));
}
void _playController(int index) async {
if (!_listeners.keys.contains(index)) {
_listeners[index] = _listenerSpawner(index);
}
_controller(index).addListener(_listeners[index]);
await _controller(index).play();
setState(() {});
}
void _previousVideo() {
if (_lock || index == 0) {
return;
}
_lock = true;
_stopController(index);
if (index + 1 < _urls.length) {
_removeController(index + 1);
}
_playController(--index);
if (index == 0) {
_lock = false;
} else {
_initController(index - 1).whenComplete(() => _lock = false);
}
}
void _nextVideo() async {
if (_lock || index == _urls.length - 1) {
return;
}
_lock = true;
_stopController(index);
if (index - 1 >= 0) {
_removeController(index - 1);
}
_playController(++index);
if (index == _urls.length - 1) {
_lock = false;
} else {
_initController(index + 1).whenComplete(() => _lock = false);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Playing ${index + 1} of ${_urls.length}"),
),
body: Stack(
children: <Widget>[
GestureDetector(
onLongPressStart: (_) => _controller(index).pause(),
onLongPressEnd: (_) => _controller(index).play(),
child: Center(
child: AspectRatio(
aspectRatio: _controller(index).value.aspectRatio,
child: Center(child: VideoPlayer(_controller(index))),
),
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _buffer,
color: Colors.grey,
),
),
Positioned(
child: Container(
height: 10,
width: MediaQuery.of(context).size.width * _position,
color: Colors.greenAccent,
),
),
],
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(onPressed: _previousVideo, child: Icon(Icons.arrow_back)),
SizedBox(width: 24),
FloatingActionButton(onPressed: _nextVideo, child: Icon(Icons.arrow_forward)),
],
),
);
}
}
All of the logic lives inside the state object, therefore makes it dirty. I might turn this into a package in the future.