The ListView shows Item1, Item2 and Item3.
When I try to delete Item2, the ListView incorrectly shows Item1 and Item2.
The console shows the correct items in the list: Item1 and Item3.
HomeScreen:
class HomeScreen extends StatefulWidget {
#override
HomeScreenState createState() => new HomeScreenState();
}
class HomeScreenState extends State<HomeScreen> {
List<Todo> todos = new List();
#override
void initState() {
super.initState();
populateTodos();
}
void populateTodos() async {
TodoDatabase db = new TodoDatabase();
db.getAllTodos().then((newTodos) {
for (var todo in newTodos) {
print(todo.title + ", " + todo.id);
}
setState(() => todos = newTodos);
});
}
void openAddTodoScreen() async {
Navigator
.push(context,
new MaterialPageRoute(builder: (context) => new AddTodoScreen()))
.then((b) {
populateTodos();
});
}
void clearDb() async {
TodoDatabase db = new TodoDatabase();
db.clearDb();
populateTodos();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Todo App"),
actions: <Widget>[
new IconButton(
icon: new Icon(Icons.delete),
onPressed: () => clearDb(),
),
new IconButton(
icon: new Icon(Icons.refresh),
onPressed: () => populateTodos(),
)
],
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Expanded(
child: new ListView.builder(
padding: new EdgeInsets.all(10.0),
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
return new TodoItem(todos[index], onDelete: (id) {
TodoDatabase db = new TodoDatabase();
print("ID: " + id);
db.deleteTodo(id).then((b) {
populateTodos();
});
});
},
),
)
],
),
),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.add), onPressed: () => openAddTodoScreen()),
);
}
}
DataBase Code:
class TodoDatabase {
TodoDatabase();
static Database _db;
Future<Database> get db async {
if (_db != null) {
return _db;
}
_db = await initDB();
return _db;
}
Future<Database> initDB() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "main.db");
var theDb = await openDatabase(path, version: 1, onCreate: createDatabase);
return theDb;
}
void createDatabase(Database db, int version) async {
await db.execute("CREATE TABLE Todos(id STRING PRIMARY KEY, title TEXT, description TEXT)");
print("Database was Created!");
}
Future<List<Todo>> getAllTodos() async {
var dbClient = await db;
List<Map> res = await dbClient.query("Todos");
print(res);
return res.map((map) => new Todo(title: map["title"], description: map["description"], id: map["id"])).toList();
}
Future<Todo> getTodo(String id) async {
var dbClient = await db;
var res = await dbClient.query("Todos", where: "id = ?", whereArgs: [id]);
if (res.length == 0) return null;
return new Todo.fromDb(res[0]);
}
Future<int> addTodo(Todo todo) async {
var dbClient = await db;
int res = await dbClient.insert("Todos", todo.toMap());
return res;
}
Future<int> updateTodo(Todo todo) async {
var dbClient = await db;
int res = await dbClient.update(
"Todos",
todo.toMap(),
where: "id = ?",
whereArgs: [todo.id]);
return res;
}
Future<int> deleteTodo(String id) async {
var dbClient = await db;
var res = await dbClient.delete(
"Todos",
where: "id = ?",
whereArgs: [id]);
print("Deleted item");
return res;
}
Future<int> clearDb() async {
var dbClient = await db;
var res = await dbClient.execute("DELETE from Todos");
print("Deleted db contents");
return res;
}
}
This almost seems like a bug with Flutter.
I have to add some more text because otherwise it won't let me post.
I don't know how to illustrate the issue further.
Thanks for your help!
I was missing a key.
Here's the correct ListView.builder:
new ListView.builder(
key: new Key(randomString(20)),
padding: new EdgeInsets.all(10.0),
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
return new TodoItem(todos[index], onDelete: (id) {
TodoDatabase db = new TodoDatabase();
db.deleteTodo(id).then((b) {
populateTodos();
});
});
},
),
Related
I'm trying to change the state of isSyncing then rebuild the widget with set state once await api.fetchProducts() is completed. api.fetchProducts() is what i used to fetch from API then store local using sqflite.
I tried using cloudSyn.then() but it wont work.
class SyncProgress extends StatefulWidget {
#override
_SyncProgressState createState() => _SyncProgressState();
}
class _SyncProgressState extends State<SyncProgress> {
bool isSyncing = true;
String progressString = 'Syncing your data....';
final db = DatabaseHelper();
final bloc = ProductBloc();
#override
void initState() {
super.initState();
}
Future cloudSync() async{
await api.fetchProducts();
//Here is the challenge
setState(() {
isSyncing = false;
progressString = 'Syncing complete....';
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: isSyncing ? _indicateProgress() : _syncDone()
);
}
Widget _indicateProgress(){
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(),
SizedBox(height: 50.0,),
Text(progressString, style: TextStyle(
fontSize: 16.0,
),),
],
),
);
}
_syncDone(){
print('Syncing completed');
//return Navigator.push(context, MaterialPageRoute(builder: (context) => HomePage()));
}
}
Use then to force setState function to execute only after fetchProducts() is finished:
Future cloudSync() async{
await api.fetchProducts().then(
setState(() {
isSyncing = false;
progressString = 'Syncing complete....';
});
);
}
How to display one by one data using this DB function?
Future<List<Data>> display() async {
//final Database db = await database;
var db = await db1;
final List<Map<String, dynamic>> maps = await db.query('syncTable');
return List.generate(maps.length, (i) {
return Data(
syn_TableName: maps[i]['syn_TableName'],
syn_ChangeSequence: maps[i]['syn_ChangeSequence'],
);
});
}
You can use the FutureBuilder to consume your display() method. Then inside the FutureBuilder you can use AsyncSnapshot.data to get your List of Dataelements.
In the next step you use can call List.map() to map your Data to widgets. In this example I use the ListTile to display:
snapshot.data.map((data) {
return ListTile(
title: Text(data.syn_TableName),
subtitle: Text(data.syn_ChangeSequence),
);
}).toList(),
Here a minimal working example which you can try out:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FutureBuilder<List<Data>>(
initialData: [],
future: display(),
builder: (context, snapshot) {
return ListView(
children: snapshot.data.map((data) {
return ListTile(
title: Text(data.syn_TableName),
subtitle: Text(data.syn_ChangeSequence),
);
}).toList(),
);
}),
),
);
}
Future<List<Data>> display() async {
return List.generate(15, (i) {
return Data(
syn_TableName: 'syn_TableName $i',
syn_ChangeSequence: 'syn_ChangeSequence $i',
);
});
}
}
class Data {
final String syn_TableName;
final String syn_ChangeSequence;
Data({this.syn_ChangeSequence, this.syn_TableName});
}
I am developing a 'todo' flutter app using BloC Architecture pattern.
My 'Home' ui displays todo list, and user can click the item's button to change the status from "todo" to "complete".
When an item is completed, it should display with another color distinct from other todos not completed.
But when I click the "complete" button, the list view is not updated.
Below is my UI code:
class HomePage extends StatelessWidget {
final TodoRepository _todoRepository;
final HomeBloc bloc;
HomePage(this._todoRepository) : this.bloc = HomeBloc(_todoRepository);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: StreamBuilder<List<Task>>(
stream: bloc.todos,
builder: (context, snapshot) {
return ListView(
children: snapshot.data.map(_buildItem).toList(),
);
}),
),
);
}
Widget _buildItem(Todo todo) {
if (todo.complete) {
return completed(todo);
} else {
return inCompleted(todo);
}
}
Widget inCompleted(Todo todo) {
return MaterialButton(
textColor: Colors.white,
color: Colors.green,
child: Text("Complete"),
onPressed: () {
bloc.done.add(todo);
}
);
}
Widget completed(Todo todo) {
return MaterialButton(
textColor: Colors.white,
color: Colors.red,
child: Text("Cancel"),
onPressed: () {
bloc.done.add(todo);
}
);
}
}
And here is my BloC class:
class HomeBloc {
final _getTodosSubject = PublishSubject<List<Todo>>();
final _doneTodoSubject = PublishSubject<Todo>();
final _cancelTodoSubject = PublishSubject<Todo>();
final TodoRepository _todoRepository;
var _todos = <Todo>[];
Stream<List<Todo>> get todos => _getTodosSubject.stream;
Sink<Todo> get done => _doneTodoSubject.sink;
Sink<Todo> get cancel => _doneTodoSubject.sink;
HomeBloc(this._todoRepository) {
_getTodos().then((_) {
_getTodosSubject.add(_todos);
});
_doneTodoSubject.listen(_doneTodo);
_cancelTodoSubject.listen(_cancelTodo);
}
Future<Null> _getTodos() async {
await _todoRepository.getAll().then((list) {
_todos = list;
});
}
void _doneTodo(Todo todo) {
todo.complete = true;
_update(todo);
}
void _cancelTodo(Todo todo) async {
todo.complete = false;
_update(todo);
}
void _update(Todo todo) async {
await _todoRepository.save(todo);
_getTodos();
}
}
It's because you don't "refresh" your list after calling getTodos() here's the modification:
HomeBloc(this._todoRepository) {
_getTodos() //Remove the adding part it's done in the function
_doneTodoSubject.listen(_doneTodo);
_cancelTodoSubject.listen(_cancelTodo);
}
Future<Null> _getTodos() async {
await _todoRepository.getAll().then((list) {
_todos = list;
_getTodosSubject.add(list); //You can actually remove the buffer _todos object
});
}
As I mention in the comment you can remove the _todos buffer but I don't want to refract to much you code.
With these few adjustents it's should work.
Hope it's help !!
Sthg makes me crazy, I try to show json products in cards and it doesn't work. Here is what I tried so far:
Product class :
class Product {
final String id;
Product({this.id});
factory Product.fromJson(Map<String, dynamic> json) {
return new Product(
id: json['id'] as String
);
}
}
JSON:
Future loadProducts() async {
final response = await http.get('https://api.stripe.com/v1/products');
return response.body;
}
The json has the following structure (data contains a list of products):
Widget:
Widget get _homeView {
return new Column(
children: <Widget>[
new FutureBuilder(
future: loadProducts(),
builder: (context, snapshot) {
List<Product> products = parseJson(snapshot.data.toString());
return !products.isEmpty
? new ProductsList(product: products)
: new CircularProgressIndicator();
}
),
...
]
);
}
List<Product> parseJson(String response) {
final parsed = json.decode(response.toString()).cast<Map<String, dynamic>>();
return parsed.map<Product>((json) => new Product.fromJson(json)).toList();
}
ProductsList class:
class ProductsList extends StatelessWidget {
final List<Product> product;
ProductsList({Key key, this.product}) : super(key: key);
#override
Widget build(BuildContext context) {
return new ListView.builder(
itemCount: product == null ? 0 : product.length,
itemBuilder: (BuildContext context, int index) {
return new Card(
child: new Container(
children: <Widget>[
new Text(product[index].id),
],
)
);
}
);
}
}
Error :
Class '_InternalLinkedHashMap' has no instance
method 'cast' with matching arguments.
Edit 1 :
I tried :
Error :
This is my usual method for parsing a json list of objects (bit simpler but it works):
List<Product> parseJson(String response) {
List<Product> products = new List<Product>();
List jsonParsed = json.decode(response.toString());
for (int i = 0; i < jsonParsed.length; i++) {
products.add(new Product.fromJson(jsonParsed[i]));
}
return products;
}
it works, can show the addTask page
var task = await Navigator.of(context).pushNamed('/addTask');
don't work, does not change the page.
String task = await Navigator.of(context).pushNamed('/addTask');
more code:
class TodoListState extends State<TodoList> {
//....
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text('Todo List')),
body: _taskList.isEmpty ? emptyView("No task") : _buildTodoList(),
floatingActionButton: new FloatingActionButton(
onPressed: _pushAddTodoScreen,
tooltip: 'Add task',
child: new Icon(Icons.add),
),
);
}
void _pushAddTodoScreen() async {
var task = await Navigator.of(context).pushNamed('/addTask');
_addTask(task);
}
void _addTask(String taskTitle) async {
AppDatabase appDatabase = AppDatabase.get();
await appDatabase.insertTask(taskTitle);
_updateTasks();
}
}
and how to see the log in the android studio, my app is running on a ios simulator
Try with explicit types:
String task = await Navigator.of(context).pushNamed<String>('/addTask');