Search code examples
androidjsonflutterdartflutter-dependencies

Flutter Inventory Management: Read from a json assets file, write to a local file and add to a Riverpod managed list of items


I am trying to learn Flutter through implementation of an inventory app. My app currently reads a list of items from a json file, managed by riverpod, this part works. I also read that Flutter does not allow writing to assets, so I wrote an insertItem() method that writes to a local file and also adds the new item to the managed current Items list. Neither of them are being generated, with no errors. I just see 'Item Added' snack bar, Inventory List is same in the inventory list screen and also file is not in the local device file explorer. I will try to briefly add the relevant parts of the code, let me know if wish to see some other parts. Reading from Json assets file part works fine.

@riverpod
class JsonInventoryService extends _$JsonInventoryService {
  late Future<List<Item>> _currentItems;
  late InventoryResult inventoryResult;
@override
  Future<List<Item>> build() {
    return Future.value([Item(id: 0, productName: 'NULL ITEM'),]);
  }

  Future loadItems() async {
    var jsonString = await rootBundle.loadString('assets/inventorylist.json');
    inventoryResult =
    InventoryResult.fromJson(jsonDecode(jsonString));
    _currentItems = Future.value(inventoryResultsToItems(inventoryResult));

  }
Future<List<Item>> fetchData() async {
    return _currentItems;
  }

INSERT ITEM METHOD:

 Map<String, dynamic> insertItem(Item item) {
    inventoryResult.results.add(item);
    final jsonmap = inventoryResult.toJson(); //json_serializable automatically handles nested class
    storage.writeFile(jsonmap);
    return jsonmap;
  }

}

Inventory Result is just a model class that has areas for item properties and toJson, fromJson that is being managed by json_serializable.

Storage class:

class Storage {
  Future<String> get _localPath async {
    final directory = await getApplicationDocumentsDirectory();

    return directory.path;
  }

  Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/inventorylist.json');
  }

  Future<File> writeFile(dynamic jsonmap) async {
    final file = await _localFile;
    return file.writeAsString('$jsonmap');
  }
}

ADD NEW ITEM DART FILE:

class AddItemButton extends ConsumerStatefulWidget {

  const AddItemButton({
    required this.image,
    required this.mode,
    required this.storage,
    super.key,
  });

  final Storage storage;
  @override
  ConsumerState createState() => _AddItemButtonState();
}

class _AddItemButtonState extends ConsumerState<AddItemButton> {
  late var inventoryService = ref.watch(serviceProvider);
  late var currentItemList = inventoryService.fetchData();

  @override
  Widget build(BuildContext context) {

    Item addNewItem({int? barcodeNo, required String productName}) {
      int id = Random().nextInt(100);
      Item newItem =
      Item(id: id, productName: productName, barcodeNo: barcodeNo, isActive: true,);
      inventoryService.insertItem(newItem);
      print("Added $productName!");
      return newItem;
    }

Storage object is being created in main.dart and being passed to relevant screens and classes. Add Item button uses insertItem() function. The screen that shows inventory items uses fetchData() function. Please let me know if needed any more clarifications.

Any suggestions to improve or fix is really appreciated as I have just started last month..

EDIT:

I have edited code as suggestions so now that build() method of the riverpod service provider returns the currentItems list, but I am unable to use this list anywhere on my app, as it throws AsyncValue<List>> cannot be assigned to List or Future type errors in several places where I used the original list with FutureBuilder, or even when I try to use state as a List. What am I missing here?

EDIT 2: I used async builder with when() expression to access the lists and now everything is working. Leaving all of these just in case someone is newly starting on Riverpod like me.


Solution

  • Think about Riverpod as composition of providers similar to Flutter's composition of widgets.

    You have quite a bit of logic trapped in non-providers/notifiers which is affecting your ability to get "rebuilds".

    This is a common fault when using Riverpod for the first time where the the build method is not properly used. It is not good practice to return data from other functions in notifiers, you want to either affect the state or invalidate to cause notifiers/providers to change. You should not be returning data from other functions.

    In order to properly get the desired effect, you will need a new notifier that tracks the STATE of the list of Items and handles the Items Modification/Additions. Then we can combine that logic together and observe Riverpod's magic.

    In your example, you are have a notifier called JsonInventoryService. Its build method is only return one item always and that is the NULL ITEM. This is the fault point, this needs to watch another provider that is in-charge of the Items. So when the Items Notifier changes, the JsonInventoryService will rebuild because it is watching that notifier.

    Here is an approach to how we can change this code so that your usage of ref.watch will properly trigger. I did not compile this code but it should get the idea across.

    Original

    @riverpod
    class JsonInventoryService extends _$JsonInventoryService {
      late Future<List<Item>> _currentItems;
      late InventoryResult inventoryResult;
    @override
      Future<List<Item>> build() {
        return Future.value([Item(id: 0, productName: 'NULL ITEM'),]);
      }
    
      Future loadItems() async {
        var jsonString = await rootBundle.loadString('assets/inventorylist.json');
        inventoryResult =
        InventoryResult.fromJson(jsonDecode(jsonString));
        _currentItems = Future.value(inventoryResultsToItems(inventoryResult));
    
      }
    Future<List<Item>> fetchData() async {
        return _currentItems;
      }
    

    Modified

    @riverpod
    class ItemsService extends _$ItemsService {
        Future<List<Item>> build() async {
            final jsonString = await rootBundle.loadString('assets/inventorylist.json');
            final inventoryResult = InventoryResult.fromJson(jsonDecode(jsonString));
            return inventoryResultsToItems(inventoryResult);
        }
    
        Future<void> addItem(Item item) async {
            //Makes a copy of the existing state list
            final current = List.from(state);
            current.add(item);
            //Updates the state such that it includes the new item
            //Causes a rebuild of anything watching via ref.watch
            state = current;
        }
      }
    
    
    @riverpod
    class JsonInventoryService extends _$JsonInventoryService {
      @override
      Future<List<Item>> build() async {
        final items = await ref.watch(itemsService.future);
        if(items.isEmpty) {
            return [Item(id: 0, productName: 'NULL ITEM'),];
        } else {
            return items;
      }
    }
    

    You should now be able to get reactiveness when the additional items are added and your JsonInventoryService should rebuild.