Trying to use Hivedb with a multi state nav bar app - flutter

I'm trying to use the hive db to store simple objects in an app that has 3 main pages, selected with a nav bar in the following form (following closely the example from the flutter docs).
/// determine body widget ie page to be rendered
int _pageIndex = 0;
/// list of body widgets
static const List<Widget> _pageOption = [
KeyList(), //index 0
PersonalKey(), //index 1
Crypt(), //index 2
];
/// [index] tells body of scaffold what widget to render
void _changePage(int index) {
setState(() {
_pageIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
widget.title,
style: const TextStyle(color: cyan),
),
),
body: Center(
child: _pageOption.elementAt(_pageIndex),
),
I'm not sure how to get the hive box to open / work with each page. for instance a Box would need to used in the KeyList() page. I tried passing the box through state but this is warned against and didn't work.
I tried varies combinations of an async main function and an overriden innitState function in _KeyListState() and now I'm not getting any widgets to render.
main function (from main.dart)
void main() async {
await Hive.initFlutter();
await Hive.openBox<ContactKey>('contacts');
Hive.registerAdapter(ContactKeyAdapter());
runApp(const MyApp());
}
reference from key_list.dart
class _KeyListState extends State<KeyList> {
late final Box contactBox;
#override
void initState() {
super.initState();
contactBox = Hive.box("contacts");
// makeKeyList();
}
#override
void dispose() {
Hive.close();
super.dispose();
}
final List<ContactKey> _keys = []; // = [
// ContactKey(contactName: "cade", publicKey: "123")
// ];
void addKey(String name, String key) async {
ContactKey newContact = ContactKey(contactName: name, publicKey: key);
setState(() {
contactBox.add(newContact);
makeKeyList();
});
}
void makeKeyList() {
if (contactBox.isNotEmpty) {
for (var i = 0; i < contactBox.length; i++) {
_keys.add(contactBox.getAt(i));
}
}
}
#override
Widget build(BuildContext context) {
return Stack(
...
I'm not getting any errors or warnings but when the app is running I get the error "each child must be laid out only once" A google search on this error made it seem like a flutter bug, but if I remove the Hive code my app renders again.
I'm pretty lost right now and if anyone has any tips or sample apps that use a nav bar with hive they'd be greatly appreciated!
Thanks

Related

how to know whether the screen is in front or background

i want to know the screen(StatelessWidget) is in front or background.
for now, a screen in background shows AlertDialog, but i want to show the dialog only when the screen is in front.
is there any way to do that?
here's my code using hooks_riverpod.
i omitted some lines because of "mostly code" warning.
final _provider = provider801x;
class MyPage801x extends HookConsumerWidget {
MyPage801x({Key? key}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final String? message = ref.watch(_provider).message;
if (message != null) {
ref.read(_provider).done();
showResult(context, message); //don't want to show when in background.
}
return Scaffold(
appBar: AppBar(
title: Text('main screen'),
),
body: Container(),
);
}
Future<void> showResult(BuildContext context, String message) async {
Future.delayed(Duration(milliseconds: 500), () async {
await showOKOnlyDialog(context, message: message);
});
}
}
final provider801x = ChangeNotifierProvider.autoDispose((ref) => Notifier801x(ref));
class Notifier801x extends ChangeNotifier {
Notifier801x(this._ref) {
_iapManager = IAPManager4Android();
_refreshDataset();
_iapListener = _iapManager.onChangedIAP.listen((IAPTransactionStateEnum state) async {
_message = state.message! + ' #801x';
notifyListeners();
});
}
String? _message = null;
String? get message => _message;
void done() {
_message = null;
}
void _refreshDataset() {
//fetch data
notifyListeners();
}
}
this code worked!
if (ModalRoute.of(context)?.isCurrent ?? false) {
showResult(context, message);
}
I got this answer from the page below. thank you all!
Flutter | How to know if widget is on top of navigation stack (visible)
You could use mounted for this case.
mounted is true if the widget is visible to the user or technically on the top of the stack of screens otherwise will be false.
So, you can use it like this way
if(mounted){
//do your stuff like show dialog or snackbar
}
Use a static string (or DI ) in main function , set it to class name in build function of any screen. check this variable to know which screen is visible.

State and Scroll position restore Flutter

I have an app which fetches posts from a site using a API and then displays it. There are three navigation options, which are basically filters.
The problem is, whenever I switch to another navigation tab (I'm using bottom navigation bar), it ends up rebuilding the whole page, meaning it will fetch all that data again and it might potentially contain new data.
What I want to do is to keep restore this data in a way that is fast and my initState() doesn't get called(because that is what fetches the data). I did try using all the different kind of keys but I cant get it to work.
Main page:
class AppHomePage extends StatefulWidget {
AppHomePage({Key? key}) : super(key: key);
#override
_AppHomePageState createState() => _AppHomePageState();
}
List<Widget> _navs = [
BestPostsRoute(key: PageStorageKey("bestP")),
HotPostsRoute(key: PageStorageKey("hotP")),
NewPostsRoute(key: PageStorageKey("newP"))
];
class _AppHomePageState extends State<AppHomePage> {
int _currentIndex = 0;
onTap(index) => {
setState(() => {_currentIndex = index})
};
#override
Widget build(BuildContext context) {
return Scaffold(
/* appbar ... */
body: _navs.elementAt(_currentIndex),
bottomNavigationBar: BottomNavigationBar(
items: [
/* nav items */
],
currentIndex: _currentIndex,
onTap: onTap,
),
);
}
}
One of the three pages(the code is similar in all three):
/* imports... */
class HotPostsRoute extends StatefulWidget {
HotPostsRoute({Key? key}) : super(key: key);
#override
_HotPostsRouteState createState() => _HotPostsRouteState();
}
class _HotPostsRouteState extends State<HotPostsRoute> {
late PostInstance postInstance;
List<Post> _posts = [];
bool _loaded = false;
fetchPosts(String? after) async {
var stream = postInstance.front.hot(limit: 10, after: after);
await for (UserContent post in stream) {
Submission submission = post as Submission;
Post pPost = Post(submission);
pPost.parse().then((value) => setState(() {
_posts.add(pPost);
}));
}
setState(() {
_loaded = true;
});
}
#override
void initState() {
super.initState();
if (mounted) {
setState(() {
redditInstance =
Provider.of<PostInstanceState>(context, listen: false)
.getInstance;
});
fetchPosts("");
}
}
// Fetches and generates posts
Widget _buildPosts() {
return ListView.builder(
itemCount: _posts.length + 1,
itemBuilder: (ctx, index) {
if (index < _posts.length) {
return _buildPost(_posts.elementAt(index));
} else {
fetchPosts(_posts.last.fullname);
return SpinKitDualRing(color: Colors.white);
}
},
);
}
// A singular post
Widget _buildPost(Post post) {
print(post.object);
return PostCard(post, key: ObjectKey(post.object)); // .object just creates a map of all fields
}
#override
Widget build(BuildContext context) {
setState(() {});
return Container(
child: _loaded ? _buildPosts() : SpinKitDualRing(color: Colors.white),
);
}
}
So I kept searching and eventually a post on Medium led me to the IndexedStack Widget.
Its a widget that is made from the Stack widget and basically loads and stores the state of all its childrens. Unlike Stack, it shows its children one at a time and thus is perfect to use with BottomNavigationBar.
Here's the Blog post for anyone looking out.

Flutter: How to share an instance of statefull widget?

I have a "WidgetBackGround" statefullwidget that return an animated background for my app,
I use it like this :
Scaffold( resizeToAvoidBottomInset: false, body: WidgetBackGround( child: Container(),),)
The problem is when I use navigator to change screen and reuse WidgetBackGround an other instance is created and the animation is not a the same state that previous screen.
I want to have the same animated background on all my app, is it possible to instance it one time and then just reuse it ?
WidgetBackGround.dart look like this:
final Widget child;
WidgetBackGround({this.child = const SizedBox.expand()});
#override
_WidgetBackGroundState createState() => _WidgetBackGroundState();
}
class _WidgetBackGroundState extends State<WidgetBackGround> {
double iter = 0.0;
#override
void initState() {
Future.delayed(Duration(seconds: 1)).then((value) async {
for (int i = 0; i < 2000000; i++) {
setState(() {
iter = iter + 0.000001;
});
await Future.delayed(Duration(milliseconds: 50));
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomPaint(painter: SpaceBackGround(iter), child: widget.child);
}
}
this is not a solution, but maybe a valid workaround:
try making the iter a static variable,
this of course won't preserve the state of WidgetBackGround but will let the animation continue from its last value in the previous screen
A valid solution (not sure if it's the best out there):
is to use some dependency injection tool (for example get_it) and provide your WidgetBackGround object as a singleton for every scaffold in your app

Flutter GetX controller not calling method when revisit the app screen

I am new to the Flutter GetX package and facing problem when using the flutter GetX package. I have several app screens where one screen for listing all the products from the database.
In my product controller, I am fetching all the data from the database and showing with listview like as given code below and It's working fine.
Problem: When I'm inserting a new record from another controller and come back to the product list controller it'll not showing newly added data. Actually this time onInit method won't fire.
Controller code
class ProductIndexCtrl extends GetxController {
var products = [].obs;
#override
void onInit() {
super.onInit();
getAll();
}
void getAll() {
Product.getAll().then((jsonList) {
products.value = Product.fromJsonList(jsonList);
});
}
}
class ProductCreateCtrl extends GetxController {
void saveData(Product product) {
...
...
//after successful insert
Get.toNamed('productIndex');
}
}
Product index screen
final ctrl = Get.put(ProductIndexCtrl());
GetX<ProductIndexCtrl>(builder: (controller) {
return _listview(controller.products);
}
Widget _listview(data) {
...
...
}
As the GetX dependency manager controls the lifecycle of dependencies, you should not manually call them. It's GetX's responsibility when to call them.
Therefore you need to manually call getAll() method of your ProductIndexCtrl controller inside the saveData() method of your ProductCreateCtrl like:
saveData(Product product){
....
....
final indexCtrl= Get.find<ProductIndexCtrl>();
indexCtrl.getAll();
}
By returning to that page, you can return the new information locally to the previous page
> Controller code
class ProductIndexCtrl extends GetxController {
var products = [].obs;
#override
void onInit() {
super.onInit();
getAll();
}
void getAll() {
Product.getAll().then((jsonList) {
products.value = Product.fromJsonList(jsonList);
});
}
}
> Product index screen
class ProductCreateCtrl extends GetxController {
void saveData(Product product) {
...
...
//after successful insert
Get.back(result: product);
}
}
and get Data when back
Get.toName('ProductCreateCtrl').then(result){
products.add(result);
}
I tried a similar thing with one single controller. The code snippet is given below.
First, create the ProductView. Since this is the entry point of the application, So you will create a GetX controller inside of this.
/// THIS IS PARENT VIEW SO WE WILL CREATE GETX CONTROLLER HERE
class ProductView extends StatelessWidget {
const ProductView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final controller = Get.put(ProductController());
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
),
body: Obx(() {
return controller.myProductList.isEmpty
? showNoProductView()
: ListView.builder(
itemCount: controller.myProductList.length,
itemBuilder: (context, index) {
return YourListItemView(controller.myProductList[index]);
},
);
}),
);
}
}
The view AddProductView is responsible for adding new products to the DB. We can assume that there is a FloatingActionButton present in ProductView and onClick on that button, we will open this AddProductView.
/// THIS IS CHILD VIEW SO WE WILL FIND THE PRODUCT CONTROLLER HERE
class AddProductView extends StatelessWidget {
const AddProductView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final controller = Get.find<ProductController>();
return Scaffold(
appBar: AppBar(
title: const Text('Add Product'),
),
body: Column(
children: [
// ADD YOUR OTHER WIDGETS TO GET PRODUCT INFO
TextButton(
child: const Text('Click to Add'),
onPressed: () {
var productName = nameTextEditingController.text;
var productQuantity = qtyTextEditingController.text;
var product = YourProductObject(productName, productQuantity);
controller.addProduct(product: product);
},
)
],
),
);
}
}
Finally, the controller will look like this.
import 'package:get/get.dart';
class ProductController extends GetxController {
// this will be your custom product list object
var myProductList = <YourProductObject>[].obs;
var dbInstance = YourDbInstance();
#override
void onReady() async {
super.onReady();
// perform database operation
await fetchDataFromDb();
}
Future<void> fetchDataFromDb() async {
// assuming that data is coming as List<YourProductListObject>
// always use try catch in db operation. for demo purpose I am skipping that.
var productListFromDb = await dbInstance.getYourProductListObjectList();
myProductList.assignAll(productListFromDb);
}
Future<void> addProduct({required YourProductObject product}) async {
// assuming that there is a function that returns true if a product is added to db
var isAdded = await dbInstance.addProduct(product);
if (isAdded) {
myProductList.add(product);
}
}
}
Since myProductList is a RxList so getx will observe it and will update the UI accordingly. You must add Obx((){}) in view.

How to force stateful widget redraw using keys?

I'm making an app that pulls data from an API and displays it in a view (MVC style).
I need to figure out how to force my view widget to redraw itself. Right now I tried with ValueKeys and ObjectKeys but to no avail.
There's lots and lots of code so I am going to use snippets as much as possible to keep it clear. (If you need to see more code feel free to ask)
Here's my view widget:
class view extends StatefulWidget{
view({
Key key,
this.count = 0,
}) : super(key: key);
int count;
String _action='';
var _actionParams='';
var _data;
Function(String) callback;
void setAction(String newAction){
_action = newAction;
}
void setActionParams(String params){
_actionParams = jsonDecode(params);
}
void setData(String data){
_data = jsonDecode(data);
}
void incrementCounter(){
count++;
}
#override
_viewState createState() => _viewState();
}
class _viewState extends State<view>{
Object redrawObject = Object();
#override
Widget build(BuildContext context) {
/*
switch(widget._action){
case '':
break;
default:
return null;
}
*/
return Text("Counter: "+widget.count.toString());
}
#override
void initState(){
this.redrawObject = widget.key;
super.initState();
}
}
You can see in the commented code that I am planning to change the way the view builds itself in function of the data that gets passed to it.
What I have tried so far is to pass a ValueKey/ObjectKey to the view from main.dart in a constructor and then changing the object at runtime. Unfortunately that did not work.
At the top of my main.dart(accessible from anywhere within main) I have this:
Object redraw = Object();
final dataView = new view(key: ObjectKey(redraw));
Then in the body of the homepage I have the view and a floating button right under.
If I press the button it should increment the counter inside the view and force it to redraw. Here's the code I have tried so far:
body: Center(
child: dataView
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.badge),
onPressed: (){
dataView.incrementCounter();
redraw = new Object();
},
),
From what I understand, if the object that was used as a key gets changed, then flutter should rebuild the state for the widget. So I'm setting my object to a new object but it's not working.
I also tried something like this:
onPressed: (){
setState((){
dataView.incrementCounter();
redraw = new Object();
});
},
Eventually I'd like to use a navigator in conjunction with my view widget (so that we have a back button) but I don't know if this is possible.
It feels a bit like I'm fighting with the framework. Is there a different paradigm I should use (like pages?) or is it possible for me to do it this way?
How do I force my view widget to get redrawn?
Using Göktuğ Vatandaş' answer and GlobalKeys I was able to figure it out.
I made a reDraw() function inside the state and then I called it from my main using a GlobalKey.
Note: Wrapping in a container and using a key for the container is not necessary. Calling setState() is enough to force a redraw.
Here's the new view widget:
import 'dart:convert';
import 'package:flutter/cupertino.dart';
GlobalKey<_viewState> viewKey = GlobalKey();
class view extends StatefulWidget{
view({
Key key,
this.count = 0,
}) : super(key: key);
int count;
String _action='';
var _actionParams='';
var _data;
Function(String) callback;
void setAction(String newAction){
_action = newAction;
}
void setActionParams(String params){
_actionParams = jsonDecode(params);
}
void setData(String data){
_data = jsonDecode(data);
}
void incrementCounter(){
count++;
}
#override
_viewState createState() => _viewState();
}
class _viewState extends State<view>{
#override
Widget build(BuildContext context) {
/*
switch(widget._action){
case '':
break;
default:
return null;
}
*/
return Text("Counter: "+widget.count.toString());
}
#override
void initState(){
super.initState();
}
void reDraw(){
setState((){});
}
}
Here's where I declare the view widget in my main:
final dataView = new view(key: viewKey);
Here's where I call the reDraw() function:
floatingActionButton: FloatingActionButton(
child: Icon(Icons.badge),
onPressed: (){
dataView.incrementCounter();
viewKey.currentState.reDraw();
},
),
Thanks Göktuğ Vatandaş!
You can check flutter_phoenix's logic for redraw effect. I think its very useful or you can just use package itself. Basically it does what you trying to achive.
It creates a unique key in state.
Key _key = UniqueKey();
Injects it to a container.
#override
Widget build(BuildContext context) {
return Container(
key: _key,
child: widget.child,
);
}
And when you call rebirth it just refresh key and that causes view to rebuild.
void restartApp() {
setState(() {
_key = UniqueKey();
});
}