I'm using MobX on Flutter to control states.
Clicking on the FloatingActionButton generates a ListView that contains CheckboxListTile.
However checkboxes are not changing their status
Could you help me fix this problem?
Below is the code and an image:
home_controller.dart
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter_modular/flutter_modular.dart';
part 'home_controller.g.dart';
#Injectable()
class HomeController = _HomeControllerBase with _$HomeController;
abstract class _HomeControllerBase with Store {
#observable
ObservableList<CheckBoxModel> mapValues = <CheckBoxModel>[].asObservable();
#observable
ListView listViewCheckbox;
#action
void listViewChekbox(value) {
for (var i in value) {
mapValues.add(CheckBoxModel(key: i));
}
}
}
class CheckBoxModel{
CheckBoxModel({this.key, this.checked = false});
String key;
bool checked;
}
home_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'home_controller.dart';
class HomePage extends StatefulWidget {
final String title;
const HomePage({Key key, this.title = "Home"}) : super(key: key);
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends ModularState<HomePage, HomeController> {
HomeController controller = HomeController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Observer(builder: (_) {
return controller.mapValues == null ? Container() : ListView.builder(
itemCount: controller.mapValues.length,
itemBuilder: (_, int index){
return CheckboxListTile(
title: Text(controller.mapValues[index].key),
value: controller.mapValues[index].checked,
onChanged: (bool value) {
controller.mapValues[index].checked = value;
},
);
},
);
}),
floatingActionButton: FloatingActionButton(
onPressed: () {
List listValues = ['foo', 'bar'];
controller.listViewChekbox(listValues);
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
According to the image, the ListView of CheckboxListTile is created but the checkboxes do not change their status.
You don't have to use setState() or anything else mobX does that for you, you just need to edit you abstract class to tell mobX main variable value changed, like next
abstract class _HomeControllerBase with Store {
#observable
ObservableList<CheckBoxModel> mapValues = <CheckBoxModel>[].asObservable();
#observable
ListView listViewCheckbox;
#action
void listViewChekbox(value) {
for (var i in value) {
mapValues.add(CheckBoxModel(key: i));
}
mapValues = mapValues;
}
}
That's how you can solve it.
the solution found was to insert setState
onChanged: (bool value) {
setState(() {
controller.mapValues[index].checked = value;
});
},
Related
I am using CheckboxListTile to show some todo item in flutter(v3.0.4). This is the code looks like:
CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
title: Text(element.name,style:element.isCompleted == 1? TextStyle(color: Colors.grey):TextStyle(color: Colors.black)),
value: element.isCompleted == 1?true:false,
checkColor: Colors.green,
selected: element.isCompleted == 1?true:false,
onChanged: (bool? value) {
if(value!){
element.isCompleted = 1;
}else{
element.isCompleted = 0;
}
TodoProvider.updateTodo(element).then((value) => {
TodoProvider.getTodos().then((todos) => {
buildTodoItems(todos)
})
});
},
))
when the user tap the CheckboxListTile item text, I want to show the todo detail information, when the user tap the checkbox, I want to make the todo task changed to complete. Now I am facing a problem is that I could not detect which part the user tap, all the way will trigger onchange event. I have already read the CheckboxListTile source code, seems no api to do this. Am I misssing something? what should I do to detect which part the user select?
You can wrap your title in a GestureDetector(). Now when the title is tapped, only the gesture detector will be run, and not the onChanged().
In this example, if you tap on the text "Checkbox" then you can see the actual checkbox value is not being updated but the GestureDetector is being called, and if you look at the console "tapped" is being printed.
Here is a complete example. I hope you understand:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}
class MyWidget extends StatefulWidget {
const MyWidget({Key? key}) : super(key: key);
#override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
var _value = false;
#override
Widget build(BuildContext context) {
return CheckboxListTile(
title: GestureDetector(
child: Text('Checkbox'),
onTap: () {
print('tapped');
// you can change the value here too
// setState(() {
// _value = !_value;
// });
},
),
value: _value,
onChanged: (bool? value) {
setState(() {
_value = value!;
});
},
);
;
}
}
The two places highlighted are the cause of the problem. In the image as shown below, after I add a task, I am not able to individually select a task, instead all the tasks that I have added get selected collectively. How do I fix this to just select the task that I click on?
This is the Tasks class that extends the ChangeNotifier:
class Tasks extends ChangeNotifier {
bool value = false;
List<String> taskList = [
'Buy Milk',
];
void addTask(String newTask) {
taskList.add(newTask);
notifyListeners();
}
}
This is the updated entire tasks.dart file:
class TaskList extends StatelessWidget {
const TaskList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<Tasks>(
builder: (context, value, child) {
return ListView.builder(
itemCount: value.taskList.length,
itemBuilder: (context, index) {
return TaskTile(
listText: value.taskList[index],
functionCallback: (newValue) {}, //Enter Function Here.
);
},
);
},
);
}
}
class TaskTile extends StatelessWidget {
String? listText;
Function(bool?)? functionCallback;
TaskTile({this.listText, this.functionCallback, Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text(
listText!,
style: TextStyle(
decoration: Provider.of<Tasks>(context, listen: false).boolValue
? TextDecoration.lineThrough
: null,
),
),
activeColor: Colors.black,
value: Provider.of<Tasks>(context, listen: false).boolValue,
onChanged: functionCallback,
);
}
}
The actual problem is that you are using the same boolean value for all the check boxes' state(true/false[weather its selected or not]).
So, when you click on one checkbox it sets the value of value(variable) to true and therefore all the checkboxes read the value from the common value (which becomes true).Therefore,every box gets selected.
Solution : You may use different variables for different check boxes' state(true/false) if the number of checkboxes is limited,otherwise go for a differnet approach.
You are getting the whole class when you call provider.
In addition, value is a global variable for the class itself, not for the items inside taskList.
So if you need to modify a Task individually you can do something like this:
class Tasks extends ChangeNotifier {
bool value = false;
List<Task> taskList = [
Task('Buy Milk'),
];
void addTask(Task newTask) {
taskList.add(newTask);
notifyListeners();
}
void deltaValue(bool b, int index) {
taskList[index].value = !taskList[index].value; // Individual task value
notifyListeners();
}
}
Instead of using a List of String you can create a new class called Task to store the values:
class Task extends ChangeNotifier {
String name;
bool value = false;
Task(this.name);
}
The last step would be to use a Widget that displays all the values stored on the List.
For example you can use ListView.builder, so you have the index and you can use it to modify the individual value of a Task:
class TaskTile extends StatelessWidget {
String? listText;
TaskTile({this.listText, Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final tasks = Provider.of<Tasks>(context, listen: false);
final taskList = tasks.taskList; //ListView helps you iterate over all the elements on the list
return ListView.builder(
itemCount: taskList.length,
itemBuilder: (context, index) {
final task = taskList[index];
return CheckboxListTile(
title: Text(
listText!,
style: TextStyle(
decoration: task.value
? TextDecoration.lineThrough
: null,
),
),
activeColor: Colors.black,
value: task.value,
onChanged: (newValue) {
Provider.of<Tasks>(context, listen: false)
.deltaValue(newValue!,index); //Problem Here.
},
);
});
}
}
The method I used was that I created an extra map in the Tasks class and defined a map called taskMap and used the strings defined in the taskList and the bool value to control TaskTile.
The addTask function is used when adding tasks to the taskList elsewhere in the program, but it also adds tasks to the taskMap.
The Tasks class:
class Tasks extends ChangeNotifier {
String? task;
List<String> taskList = [
'Buy Milk',
];
Map<String, bool> taskMap = {
'Buy Milk': false,
};
void addTask(String newTask) {
taskList.add(newTask);
taskMap[newTask] = false;
notifyListeners();
}
void deltaValue(String newTask) {
taskMap[newTask] = !taskMap[newTask]!;
notifyListeners();
}
}
The entire tasks.dart file:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todoey_flutter/main.dart';
class TaskList extends StatelessWidget {
const TaskList({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<Tasks>(
builder: (context, value, child) {
return ListView.builder(
itemCount: value.taskList.length,
itemBuilder: (context, index) {
return TaskTile(
listText: value.taskList[index],
functionCallback: (newValue) {
value.deltaValue(value.taskList[index]);
},
);
},
);
},
);
}
}
class TaskTile extends StatelessWidget {
String? listText;
Function(bool?)? functionCallback;
TaskTile({this.listText, this.functionCallback, Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text(
listText!,
style: TextStyle(
decoration:
Provider.of<Tasks>(context, listen: false).taskMap[listText]!
? TextDecoration.lineThrough
: null, //Bool value defined in the taskMap used.
),
),
activeColor: Colors.black,
value: Provider.of<Tasks>(context, listen: false).taskMap[listText], //Bool value defined in the taskMap used.
onChanged: functionCallback,
);
}
}
I can't figure out how to make ListView items choosable like in screenshots:
This should work. On long press using GestureDetector you should alter a property of the model in the list and set state. Showing the checkbox when that condition is true, otherwise use an empty container. You can then do whatever you need within that checkbox specifically.
import 'package:flutter/material.dart';
class TEST extends StatefulWidget {
const TEST({Key? key}) : super(key: key);
#override
State<TEST> createState() => _TESTState();
}
class _TESTState extends State<TEST> {
List<MyItem> _objs = [MyItem(id: '', isSelected: false)];
bool _showCheckboxes = false;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _objs.length,
itemBuilder: (context, index) {
var item = _objs[index];
return GestureDetector(
onLongPress: () {
setState(() {
_showCheckboxes = true;
});
},
child: ListTile(
leading: _showCheckboxes
? Checkbox(
value: item.isSelected,
onChanged: (val) {
setState(() {
item.isSelected = !item.isSelected;
});
})
: Container()),
);
},
);
}
}
class MyItem {
String id;
bool isSelected;
///other properties...
MyItem({required this.id, required this.isSelected});
}
Edit: Shows checkboxes for all items, as required in question.
You can give it a leading icon by giving it a ListTile.
I am not able to make RadioListTile works. It isn't selected and unselected on click
Can you help me?
Here is me code
edit
...
final ownedController =TextEditingController();
...
RadioListTile<
String>(
value:'not owned',
groupValue:ownedController.text,
toggleable:true,
title: const Text('Owned'),
onChanged:(String) {
cubit.changeOwned(ownedController.text);
}),
...
cubit
...
bool isOwned = false;
String changeOwned(String owned) {
isOwned = !isOwned;
if (isOwned == true) {
owned = 'owned';
} else {
owned = 'not owned';
}
return owned;
}
...
Here is an example based on the enum it is more flexible and easier to add more objects in the future. Enum values can also be converted to the string and represented your user interface in a readable form.
Just copy and paste into DartPad to play with it:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => SomeCubit(),
child: const MaterialApp(
home: SomeView(),
),
);
}
}
class SomeView extends StatelessWidget {
const SomeView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My App')),
body: BlocBuilder<SomeCubit, SomeStatus>(
builder: (context, state) {
return Column(
children: [
for (var value in SomeStatus.values)
RadioListTile<String>(
title: Text(value.titleByIndex), // <- Title by extension.
value: value.name,
groupValue: state.name,
toggleable: true,
selected: value.name.contains(state.name),
onChanged: context.read<SomeCubit>().onChanged,
),
],
);
},
),
);
}
}
enum SomeStatus { owned, notOwned }
class SomeCubit extends Cubit<SomeStatus> {
SomeCubit() : super(SomeStatus.notOwned);
void onChanged(String? name) {
emit(SomeStatus.values.byName(name ?? state.name));
}
}
extension SomeStatusEx on SomeStatus {
// A useful function for converting value names to UI view.
// List ordering must contain enum values.
String get titleByIndex => ['Owned', 'Not Owned'].elementAt(index);
}
With Dart 2.17 and above:
// Dart 2.17 can convert enum value to any value
// and you do not need to create an extension to put a nicer value name to the view.
enum SomeStatus {
owned('Owned'),
notOwned('Not Owned');
const SomeStatus(this.label);
// View in the user interface "title: Text(value.label)"
final String label;
}
you can avoid using cubit to switch state.
...
final ownedController = TextEditingController();
bool isOwned = false;
String get ownedString => isOwned ? 'owned' : 'not owned';
...
RadioListTile<String>(
value: ownedString,
groupValue: 'owned',
toggleable: true,
title: Text(ownedString),
onChanged: (x) {
setState(() {
isOwned = !isOwned;
ownedController.text = ownedString;
});
},
),
...
you have to fix the toggleGroupe to 'owned' otherwise you'll have wrong display.
Let's say I create a new screen team_screen which is the first parent of the tree.
Now for my team screen there are many widgets, some of theme have their own request, I want to show loader until every widget/request finished and ready.
I thought on 2 approaches.
All the requests are executed in team_screen with future builder and I pass the props to my widgets by demand.
Every widget with request get function that get executed in the async function in the initState function, then in my parent I make to every widget state parameter that is equal to true by the function I passed and when all is don't I stop the loader.
To sum up my problem is how to maintain a widget with many children and requests and showing one loader for entire page, making all the request on same widget? Pass isInitialize function to every widget?.
Which approach is better and if there are more approaches, I would like to hear.
Thank you for your help
Example for the second approach:
import 'package:flutter/material.dart';
import 'package:info_striker/locator.dart';
import 'package:info_striker/models/fixture/fixture.dart';
import 'package:info_striker/models/odds/bookmaker.dart';
import 'package:info_striker/models/synced-team/synced_team.dart';
import 'package:info_striker/services/fixture_service.dart';
import 'package:info_striker/utils/date_utilities.dart';
class TeamNextMatch extends StatefulWidget {
Function isInitialized;
SyncedTeam team;
TeamNextMatch({
Key? key,
required this.isInitialized,
required this.team,
}) : super(key: key);
#override
State<TeamNextMatch> createState() => _TeamNextMatchState();
}
class _TeamNextMatchState extends State<TeamNextMatch> {
Fixture? _fixture;
Bookmaker? _matchResult;
bool _isInitialized = false;
#override
void initState() {
super.initState();
init();
}
init() async {
final response = await locator<FixturesService>().getData(widget.team.id);
if (response != null) {
setState(() {
_fixture = Fixture.fromMap(response["fixture"]);
_matchResult = Bookmaker.fromMap(response["matchResultOdds"]);
});
}
widget.isInitialized(true);
}
#override
Widget build(BuildContext context) {
String? _date;
bool show = _fixture != null && _matchResult != null;
_fixture != null ? "${DateUtilities.getShortDateString(_fixture!.date)}, ${DateUtilities.getTimeString(_fixture!.date)}" : null;
return show
? Column(
children: [
Text(_fixture?.league?["name"]),
if (_date != null) Text(_date),
],
)
: const SizedBox();
}
}
You can show loader as described below -
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/data_model.dart';
import 'package:http/http.dart' as http;
class APiTest extends StatefulWidget {
const APiTest({Key? key}) : super(key: key);
#override
_APiTestState createState() => _APiTestState();
}
class _APiTestState extends State<APiTest> {
final String _url = "https://jsonplaceholder.typicode.com/todos/";
bool _isLoading = true;
final List<DataModel> _allData = [];
#override
void initState() {
super.initState();
_initData().then((value) {
setState(() {
_isLoading = false;
});
});
}
Future<void> _initData() async {
final response = await http.get(Uri.parse(_url));
final List res = jsonDecode(response.body);
res.forEach((element) {
_allData.add(DataModel.fromJson(element));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Loading Demo"),
),
body: Stack(
children: [
ListView.separated(
itemCount: _allData.length,
controller: ScrollController(),
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: ((context, index) {
return ListTile(
tileColor: Colors.grey[200],
title: Text(_allData[index].title!),
subtitle: Text(_allData[index].id.toString()),
);
}),
),
if (_isLoading)
const Center(
child: CircularProgressIndicator(),
)
],
),
);
}
}