Flutter GetX controller not calling method when revisit the app screen - flutter

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.

Related

How to fetch and keep updated data with flutter_hooks?

I'm studying flutter_hooks library. My aim is to fetch data into model and be able to refresh it any time. Also I'd like to refresh data from different screens. Currently I do it this way:
Page class:
class NewPage extends HookWidget{
#override
Widget build(BuildContext context) {
final holder=useState<ModelHolder>(ModelHolder()).value;
final notifier=useListenable(holder.notifier);
final model=notifier.value;
final refresh=useState<bool>(false);
useEffect(() {
print('refetching');
holder.fetch();
},[refresh.value]);
print('model:$model, ${model.hashCode}');
return Scaffold(
appBar: AppBar(
title: Text('Page 2'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
refresh.value=!refresh.value;//trigger refresh
},
child: const Icon(Icons.add),
),
);
}
}
ModelHolder class:
class ModelHolder {
final ValueNotifier<Model?> notifier = ValueNotifier<Model?>(null);
void fetch() async {
await Future.delayed(const Duration(seconds: 2));//do server call
notifier.value=Model('data');
}
}
Model class:
class Model{
final String value;
Model(this.value);
}
Basically this code works as expected. The only thing I'm confused is refresh variable in the NewPage. Due to this I can't extract hook's logic into custom hook. This is a typical task. I suspect that I'm inventing a wheel. What is the right way of fetching/updating data with flutter_hooks?
UPD: I moved refresh property into ModelHolder and now able to isolate everything in a custom hook.
holder:
class ModelHolder {
final ValueNotifier<bool> refresher;
final ValueNotifier<Model?> notifier = ValueNotifier<Model?>(null);
ModelHolder(this.refresher);
void fetch() async {
await Future.delayed(const Duration(seconds: 2));//do server call
notifier.value=Model('data');
}
}
Custom hook:
ModelHolder useModel(){
final refresh=useState<bool>(false);
final holder=useState(ModelHolder(refresh)).value;
useListenable(holder.notifier);
useEffect(() {
print('refetching');
holder.fetch();
},[refresh.value]);
return holder;
}
Usage:
Widget build(BuildContext context) {
final holder=useModel();
...
}
And I can pass holder to other routes to update it from there.

Getx Controller not fetching data

So i have an implementation where i pass the id of a parent category and it fetches its child category from the database and displays them in another screen (view).
When i click on the parent, it sends the its category ID to the screen whose code is below and the it suppose to get the data from the database to be used in the screen. However, it says null when you click from the previous page to this new screen. If you directly do a hot reload, the data gets displayed.
Does it mean the below code doesn't get run when the screen is first loaded?
Note that all controllers and repositories have been set in the dependecies class and initialised in the main.
var childCatItem =
Get.find<ChildCategoriesController>().getChildCategory(childCatId);
Parent Category Screen Function to Pass ID.
The below code gets the ID from the already loaded data.
onTapLeftService: (){
int childCatId = randomController.isLoaded ? randomController.randomServicesList[0].children[0].id : 0;
Get.toNamed(RouteHelper.getChildCategoryServicesPage(childCatId));
},
The below code is the route where it is gotten and passed to the screen
//This is the route implementation. This is called in the above code.
static String getChildCategoryServicesPage(int childCatId) =>
'$childCategoryServicesPage?childCatId=$childCatId';
GetPage(
name: childCategoryServicesPage,
page: () {
var childCatId = Get.parameters['childCatId'];
return ChildCategoryServicesScreen(
childCatId: int.parse(childCatId!));
},
transition: Transition.fadeIn),
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:personal_start/controller/nonemergency_services/child_categories_controller.dart';
import 'package:personal_start/helper/app_styles.dart';
import 'package:personal_start/widget/title_text.dart';
class ChildCategoryServicesScreen extends StatelessWidget {
int childCatId; //Here I receive the ID of the parent category into this screen
ChildCategoryServicesScreen({Key? key, required this.childCatId})
: super(key: key);
#override
Widget build(BuildContext context) {
var childCatInstance = Get.find<ChildCategoriesController>(); // I create an instance of the controller
var childCatItem =
Get.find<ChildCategoriesController>().getChildCategory(childCatId); //Here I make an API call to the database, passing the parent category ID.
print(childCatItem);
print(childCatInstance.childCategory!.title); //I receive a null value here.
return Scaffold(
appBar: AppBar(
title: TitleTextWidget(titleText: 'Child Cat Title'),
leading: GestureDetector(
onTap: () {
Get.back();
},
child: const Icon(Icons.arrow_back_outlined),
),
backgroundColor: AppStyles.appPrimaryColor,
),
// body: !childCatItem.isLoaded
// ? const CustomLoader()
// : Center(child: TitleTextWidget(titleText: 'It has Loaded')),
);
}
}
you can pass data using getx argument
https://www.kindacode.com/article/using-getx-get-for-navigation-and-routing-in-flutter/
Try this way
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:personal_start/controller/nonemergency_services/child_categories_controller.dart';
import 'package:personal_start/helper/app_styles.dart';
import 'package:personal_start/widget/title_text.dart';
class ChildCategoryServicesScreen extends GetView<ChildCategoriesController> {
ChildCategoryServicesScreen({Key? key}): super(key: key);
#override
ChildCategoriesController controller = Get.put(ChildCategoriesController());
#override
Widget build(BuildContext context) {
...//TODO: YOUR CODE HERE
}
ChildCategoriesController
class ChildCategoriesController extends GetxController {
late int childCatId;
...//TODO: YOUR CODE HERE
#override
void onInit() {
int childCatId = Get.arguments[0];
...//TODO: YOUR CODE HERE
super.onInit();
}
#override
void onClose() {}
...//TODO: YOUR CODE HERE
}

Calling async event in flutter_bloc

I am trying to fetch data from API as soon as the flutter app loads but I am unable to achieve so
class MarketBloc extends Bloc<MarketListEvent, MarketListState> {
MarketBloc() : super(MarketLoading()) {
on<MarketSelectEvent>((event, emit) async {
emit(MarketLoading());
final data = await ApiCall().getData(event.value!);
globalData = data;
emit(MarketDataFetched(marDat: globalData.data, dealType: event.value));
});
}
}
I have called MarketLoading state as the initial state and I want to call MarketSelectEvent just after that but in the current code, action is required to do so and i want to achieve it without any action.
You have 2 options:
add an event from the UI as soon you instantiate the MarketBloc
MarketBloc()..add(MarketSelectEvent())
add an event in the initialization code
MarketBloc() : super(MarketLoading()) {
add(MarketSelectEvent());
}
You could do this with in the initState of whatever the first page is that your app loads.
class TestPage extends StatefulWidget {
#override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
late MarketBloc marketBloc;
#override
void initState() {
super.initState();
marketBloc = BlocProvider.of<MarketBloc>(context);
marketBloc.add(MarketSelectEvent());
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: BlocBuilder<MarketBloc, MarketListState>(
builder: (context, state) {
if (state is MarketLoading) {
return Text('loading...');
}
if (state is MarketDataFetched) {
return ...your UI that contains data from API call
}
},
),
),
);
}
}

How to make Flutter with GetX wait until data is loaded

I'm looking for a best-practice way to implement waiting for my app to initialize data before displaying the first page.
The app has a main controller as well as a controller per page. The main controller initially loads data from a server, and until that's done I'd like to display a splash page (or at least wait before opening the actual app-page)
An simple solution would be, that a page waits for the main controller to be initialized
class MainController extends GetxController {
final isInitialized = false.obs;
#override
void onInit() async {
Future f1 = server.get('service1').then(....)
Future f2 = server.get('service1').then(....)
Future f3 = server.get('service1').then(....)
await Future.wait([f1, f2, f3]);
isInitialized.value = true;
}
}
And a page component could:
class HandleTaskPage extends GetView<HandleTaskPageController> {
#override
Widget build(BuildContext context) {
MainController mainController = Get.find();
return Obx(() {
if (mainController.isInitialized().value) {
return TaskPanelWidget();
} else {
return WaitingPage();
}
})
}
}
But my app allows the user to start at any given page using a direct url (web-app) e.g. http://app.com/showtask/123
Which means that I must put the wait for global controller on every page.
Is there some way I could simply make Get wait (and possibly display a Welcome page) until GlobalController is ready before moving on to the page described in the route?
I've tried to add a WelcomePage to GetMaterialApp, to stop the app from going directly to the requested url. The WelcomeController should then await MainController before redirecting. But even though welcome-page does get rendered, the app still automatically continues to the page requested in the url.
void main() {
runApp(GetMaterialApp(
home: WelcomePage(),
...
...
...
You can implement mixin with name StateMixin.
Example (Controller):
class UserController extends GetxController with StateMixin {
getData() {
// make status to loading
change(null, status: RxStatus.loading());
// Code to get data
await service.getData()
// if done, change status to success
change(null, status: RxStatus.success());
}
}
Example (UI):
class HomePage extends GetView<UserController> {
#override
Widget build(BuildContext context) {
// controller from GetView
return controller.obx((state) {
return OtherWidget()
},
onLoading: CircularProgressIndicator(),
)
}
}
You can use the builder method on MaterialApp to build/show the widget you want.
Using GetX - you can show a loader while waiting for your data to load as follows:
Widget build(BuildContext context) {
final MainController mainController = Get.find();
final isInitialized = mainController.isInitialized().value;
return GetMaterialApp(
title: 'Example',
debugShowCheckedModeBanner: false,
theme: ...,
initialRoute: '/',
getPages: [...],
builder: (context, child) => isInitialized
? Container(child: child)
: const Center(
child: CircularProgressIndicator(color: Colors.blue),
),
);
}
The above should display a blue loader until isInitialized becomes true.

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();
});
}