memory_and_json_directories
A Flutter package, with a platform independent directory structure, which can be saved as JSON. This package is an implementation of a general tree.
Please Post Questions on StackOverflow, and tag @CatTrain (user:16200950)
Basic Example
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:memory_and_json_directories/memory_and_json_directories.dart';
void main() {
// build a tree in memory
MAJNode root = MAJNode(
name: "root",
child: MAJDirectory(),
);
root
.addChild(
MAJNode(
name: "one",
child: MAJDirectory(),
),
)
.addChild(
MAJNode(
name: "one one level down",
child: MAJDirectory(),
),
);
root.addChild(
MAJNode(
name: "two",
child: MAJDirectory(),
),
);
// convert to json
String treeAsJson = jsonEncode(
root.breadthFirstToJson(),
);
// load from json string
MAJNode fromJson = MAJNode.breadthFirstFromJson(
jsonDecode(
treeAsJson,
),
);
// display the tree loaded from json
runApp(
MaterialApp(
home: Scaffold(
body: MAJBuilder(
root: fromJson,
),
),
),
);
}
Custom Item Example
-
see example for full code
-
requires Basic Example code
-
create a custom item
- ensure it implements
MAJItemInterface
- this example has a back button that navigates to the parent node, and text that displays "Hello I am a custom item"
/// A basic custom item that can display whatever you wish class CustomItem implements MAJItemInterface { /// not required, but recommended static const String typeName = "custom_item"; @override String getTypeName() { return typeName; } @override Widget majBuild({ required BuildContext context, required MAJNode nodeReference, }) { return Center( child: Column( children: [ // back button, navigates to parent, unless there is not parent node ElevatedButton( onPressed: () { if (nodeReference.parent != null) { context.read<MAJProvider>().navigateToByNode( nodeTo: nodeReference.parent!, ); } }, child: const Text("Back"), ), // custom widget to display const Text("Hello I am a custom item"), ], ), ); } }
- ensure it implements
-
add the custom item's definition, so it can be built from json
// define the custom item, so it can be loaded from json MAJNode.addDefinition( typeName: CustomItem.typeName, function: () => CustomItem(), );
-
add a custom item to the tree
// add a custom item to the tree root.addChild( MAJNode( name: "Custom Item", child: CustomItem(), ), );
Data and Shared Data Example
- see example for full code
- requires Basic Example, and Custom Item Example Code
- modify
CustomItem
-
set code in build to set a default data value
// set default data value if no data if (nodeReference.data == null || nodeReference.data!.keys.isEmpty) { nodeReference.data = nodeReference.data = <String, bool>{ "pressed": false, }; }
-
wrap returned
Widget
in a StatefulBuilder to allow dynamic button changes -
add text that shows the shared data for
CustomItem
Text( nodeReference.getSharedData().toString(), ),
-
add button that shows the data for the instance of
CustomItem
// add button that shows the data for the instance of CustomItem OutlinedButton( // update data value for instance on pressed to opposite of what it was onPressed: () { setState(() { nodeReference.data!["pressed"] = !nodeReference.data!["pressed"]; }); }, // display data for this instance child: Text( nodeReference.data!["pressed"].toString(), ), ),
-
set shared data for
CustomItem
// set shared data for custom Item in memory // will be loaded from json below MAJNode.setSharedData( typeName: CustomItem.typeName, data: { "data": "Shared Data value", }, );
-
Multiple Trees Example
-
see example for full code
-
requires Basic Example, Custom Item Example, and Data and Shared Data Example code
-
create shared data for
CustomItem
in the second tree- see how a mapKey is set, this is so the name space doesn't collide with the first tree
// set shared data for custom Item in memory // will be loaded from json below // map key for tree 2 set MAJNode.setSharedData( typeName: CustomItem.typeName, data: { "data": "Shared Data value from tree 2", }, mapKey: "tree_2", );
-
create a second tree, and show how it can be converted to json, and built in memory from json
- see how a mapKey is set, this is so the name space doesn't collide with the first tree
// create 2nd tree's root MAJNode root2 = MAJNode( name: "root 2", child: MAJDirectory(), mapKey: "tree_2", // because second tree, avoids naming collisions ); root2 .addChild( MAJNode( name: "one", child: MAJDirectory(), mapKey: "tree_2", ), ) .addChild( MAJNode( name: "One down from one", child: CustomItem(), mapKey: "tree_2", ), );
-
add a second
MAJBuilder
- see how a mapKey is set, this is so the name space doesn't collide with the first tree
// tree 2, see how it has a map key, this is so the name space // doesn't collide with the first tree. tree 1 uses the default map key MAJBuilder( root: fromJson2, mapKey: fromJson2.mapKey, ),
Building a Custom Directory Item
-
see example for full code
-
requires Basic Example, Custom Item Example, Data and Shared Data Example code, and Multiple Trees Example
-
Build
CustomDirectory
,Widget
, andState
/// An example of how to create a custom directory, which will display /// in a manner you desire class CustomDirectory implements MAJItemInterface { /// not required, but recommended static const String typeName = "custom_directory"; @override String getTypeName() { return typeName; } @override Widget majBuild({ required BuildContext context, required MAJNode nodeReference, }) { // return custom directory widget return CustomDirectoryWidget( nodeReference: nodeReference, context: context, ); } } /// widget that displays the custom directory class CustomDirectoryWidget extends StatefulWidget { final MAJNode nodeReference; final BuildContext context; // get node reference, and context to pass to the state const CustomDirectoryWidget({ super.key, required this.nodeReference, required this.context, }); @override State<StatefulWidget> createState() { return CustomDirectoryWidgetState(); } } /// state of the custom directory class CustomDirectoryWidgetState extends State<CustomDirectoryWidget> { @override Widget build(BuildContext context) { /// builds the buttons that are displayed in the directory widget List<Widget> buildButtons() { List<Widget> temp = []; // add back button temp.add( // back button, navigates to parent, unless there is not parent node ElevatedButton( onPressed: () { if (widget.nodeReference.parent != null) { context.read<MAJProvider>().navigateToByNode( nodeTo: widget.nodeReference.parent!, ); } }, child: const Text("Back"), ), ); // add children of the current directory for (int i = 0; i < widget.nodeReference.children.length; i++) { temp.add( // build the node when button pressed OutlinedButton( onPressed: () { context.read<MAJProvider>().navigateToByNode( nodeTo: widget.nodeReference.children[i], ); }, // display node's path child: Text( widget.nodeReference.children[i].path, ), ), ); } return temp; } // return column of buttons that when pressed load nodes return Column( children: buildButtons(), ); } }
-
add definition
// define the custom directory, so it can be loaded from json MAJNode.addDefinition( typeName: CustomDirectory.typeName, item: () => CustomDirectory(), );
-
add to tree
// add custom dirs root2.addChild( MAJNode( name: "Custom Dir 1", child: CustomDirectory(), mapKey: "tree_2", ), ) // add to custom 1 ..addChild( MAJNode( name: "Custom Dir 1", child: CustomDirectory(), mapKey: "tree_2", ), ) // add to custom 1 ..addChild( MAJNode( name: "Custom Dir 2", child: CustomDirectory(), mapKey: "tree_2", ), // add to custom 2 ).addChild( MAJNode( name: "Custom Dir 3", child: CustomDirectory(), mapKey: "tree_2", ), );
MAJNode
-
Naming rules
MAJNode.name
must match the following regex expression:^[a-zA-Z0-9_]+( [a-zA-Z0-9_]+)*$
- each name must be unique amongst its peers
- when new nodes are added their name can not match a root node's name in their tree, because before they are added to another node as a child, they are a peer of all root nodes
- These rules are scoped to within the
MAJProvider.maps
map the nodes are referenced in
-
Stored in memory and json, see storage below for data
-
MAJNode
is the node used in the general tree -
See code for functions
MAJItemInterface
- The interface used to ensure children of MAJNode have the correct functions
- it is recommended to add a static type name variable, but it is not required
- ex:
static String typeName = "my_custom_item";
- ex:
String getTypeName
- must return a string which will be a unique key in MAJNode.definitions map
Widget majBuild
- return a widget which you wish to have displayed
- nodeReference contains any data you would need from a constructor
MAJBuilder
- This is the widget used to display your tree in memory
- makes user of Provider to receive change notifications from
MAJProvider
- this is how the widget knows which
MAJNode
to display
- this is how the widget knows which
- This widget takes two parameters
required MAJNode root
- The node you wish to have displayed first when the widget builds
- This is typically the root of the tree
String mapKey = MAJProvider.defaultMapKey
- the key you use to reference the map of nodes
- don't set if you only intend to have one tree
- set if you wish to have more than one tree in memory at once
MAJProvider
- The ChangeNotifier that notifies
MAJBuilder
to display aMAJNode
- Only exists in memory
static const defaultMapKey = "default";
- the default map key used in
MAJProvider.maps
- the default map key used in
static Map<String, Map<String, MAJNode>> maps
- The outer map references a unique key for each tree
- if there is only one tree the default map key is used
- If there is to be more than one tree in memory at a time, ensure each tree has a key in the outer map
- The inner maps contain the path of each node in the tree, and a memory reference to that node, so any node can be accessed O(1)
- The outer map references a unique key for each tree
MAJNode
references can be added and removed manually toMAJProvider.maps
, but it is not recommendedstatic void addToMap
static MAJNode? removeFromMap
- allows navigation to
MAJNode
by path or reference- by path
navigateTo
- uses map key provided by
MAJBuilder
to determine, which tree the node is in - ex:
context.read<MAJProvider>().navigateTo("/root/node");
- uses map key provided by
- by
MAJNode
referencenavigateToByNode
- navigates to the node regardless of which tree it is in
- ex:
context.read<MAJProvider>().navigateToByNode(myNode);
- by path
Storage
-
Memory Storage
- The directory is stored as a general tree
- All nodes have access to
MAJNode.sharedData
static Map<String, Map<String, dynamic>> sharedData
- Outer map
- keys refer to mapKey used by MAJProvider.maps to differentiate between trees in memory
- the keys in the outer map must match the ones used in
MAJProvider.maps
- Inner maps implements
MAJItemInterface
- keys must be a type name of a item that
- the data is any json serializable data
- can be applied to all nodes of that type in the same tree
- tree differentiated using outer map
- Each node has
name
- a
String
that is the name of the node - must follow
MAJNode
naming rules above
- a
path
- a
String
that contains the node's name and the name of all it's ancestors - must be unique amongst all nodes in the tree
- a
parent
- a reference to the node which is this node's parent
- if
null
this means the node is a root node
children
- array of nodes which are the children of the node
- if the array is empty the node has no children
typeName
- a
String
, which is used to determine which definition to use when building the node from json
- a
child
- an
object
, which implementsMAJItemInterface
- the object builds a widget when it's
build
method is called by the node
- an
data
- A
Map<String, dynamic>
- stores data required to build the node's child
- can be
null
if not required - must be able to convert to a JSON object
- number, boolean, string, null, list or a map with string keys
- A
mapKey
- a
String
that identifies, which tree the node belongs to - must match a key used in
MAJProvider.maps
outer map
- a
-
JSON storage
- The directory is stored as an object with nodes and shared data.
sharedData
- a map containing data shared within each tree
- see
MAJNode.sharedData
in memory storage
nodes
- an array of json objects stored in in left to right, top to bottom order (breadth first)
- Each node in the array has:
name
- see
name
in memory storage
- see
path
- see
path
in memory storage
- see
typeName
- see
typeName
in memory storage
- see
parent
- the path of the current node's parent
- if is empty the node is a root node
data
- a JSON object containing the data required to build the node
- see
data
in memory storage
mapKey
- see
mapKey
in memory storage
- see
- ex:
{"nodes":[{"name":"root","path":"/root","parent":"","typeName":"maj_directory","mapKey":"default","data":{}}],"sharedData":{}}
- The directory is stored as an object with nodes and shared data.