Mobile Apps for the Supply Chain

Mobile Apps for the Supply Chain

Don’t Limit Yourself

While computing in the supply chain is nothing new, many of the existing systems and platforms are typically running on bulky and dated equipment, limiting the flexibility and efficiency of employees. This issue isn’t confined to one segment of the supply chain. Everyone from suppliers to retailers is often conducting day-to-day operations with outdated supply chain applications and hardware. With the ubiquity of mobile technology, introducing apps into the supply chain can be beneficial and seamless. Mobile supply chain apps can lead to improvements at every step, often in similar ways across segments. For example, inventory management apps allow suppliers to track their raw materials, manufacturers to decrease the time to manufacture and ship, and retailers to more effectively track their stock.

Companies frequently focus the majority of their mobile technology investment on consumer-facing apps, often at the expense of mobilizing their supply chain operations. Shockoe has partnered with several big box retailers, as their supply chain app developer, to create apps that support supply chain workforce and processes. The infographic, below, outlines examples of how these supply chain apps can support desired customer experiences by improving processes such as inventory management, production planning, material management, and resource planning processes, to name a few.

See The Potential

As a supply chain app developer, we work with our clients to help them gain insight into points along the supply chain that will benefit from mobile app investment in order to get customers their product faster, more efficiently, and more effectively. This leads to a better customer experience, which maximizes their existing investment in consumer-facing solutions and increases customer loyalty.

How Suppliers Benefit:

  • Accurate sourcing: track and distribute raw materials anytime, anywhere. Mobile apps connect manufacturers to the first step of the supply chain, the raw materials supplier.
  • Easier inventory control: inventory management with the tap of a finger. Warehouse management apps give you 24/7 control of inventory
  • Simplified logistics: order fulfillment that’s simple, intuitive, and on the go. Track the transit of materials from anywhere in the field.

How Manufacturers Benefit:

  • Faster production times: supply chain apps integrate with legacy systems to cut down on the overall production process, decreasing the time to manufacture and ship.
  • Recall/damage control: simplify field assessments and integrate data captured in the field from mobile apps with back office applications. The faster a customer complaint is resolved, the more likely customers are to become repeat customers.
  • Better final product: tighter quality control means better oversight. All of this amounts to a higher quality product.

How Distributors Benefit:

  • Improved warehouse management
  • Smarter communication
  • Increased “on-time” delivery

How Retailers Benefit:

  • Better stock management
  • More efficient front-line employees
  • Less shrinkage

How Consumers Benefit:

  • Faster shipments
  • Accurate order tracking
  • Great customer experience

Increase Your Bottom Line

Partnering with an experienced supply chain app developer to digitize the supply chain means organizations can have the best of both worlds by increasing efficiency to decrease costs and making sure customers become ambassadors for the brand, leading to repeat business and long-term revenue growth.

 

Apps for the Supply Chain

 

Asset/Inventory Management Apps in Record Time with Flutter

Asset/Inventory Management Apps in Record Time with Flutter

Shockoe specializes in utilizing tools which can most efficiently provide a beautiful experience for a given project. We have a history with cross-platform frameworks, as they can often quicken the development period for a mobile app considerably. Many of our projects were historically built on Titanium, and a few more recently were undertaken with React-Native.

When Flutter was announced, we knew we had to keep a close eye on it, and we were eager for it to reach the point where it was mature enough to build a robust production app. The results were astounding. Not only does it ease many of development pains present in other cross-platform frameworks, it also gives you beautiful UI out of the box, and extraordinary speed as it is blazingly fast to develop. In fact, the entirety of the development you will see later in this post was completed single-handedly in a matter of hours!

Flutter for Inventory Management Apps

At Shockoe, we have a point of creating great inventory/asset apps the help manage resources, assets, and inventory at a number of large-scale companies. A few reasons why Flutter has been a particularly great fit for developing these kinds of apps include:

  • List Convenience: Turning a raw data list of assets and inventory into an actual list laid out on screen couldn’t be easier. It can be accomplished in a handful of lines of code.
  • Beautiful by Default: Apps in this category have a heavy focus on functionality. A framework which looks good in its most basic state lets you focus on the utility and devote as much time as you decide to enhance UI and delivering the right content.
  • List Performance: Flutter renders every pixel on the screen itself, allowing for a performance unparalleled by other frameworks. It touts its ability to maintain 60fps, and scrolling is buttery-smooth even on massive lists.
  • Empty/Loading State Simplicity: Most screens will be heavily data-driven. In some environments, displaying states like waiting for an API response or failing to connect can become extremely cumbersome. Flutter makes it easy to build a UI which reacts to these in-between moments gracefully.
  • Object-Oriented: Unlike Titanium and React-Native, which use Javascript, Flutter apps are written in Dart. This offers a number of benefits, like the lessened runtime error rate of strongly typed languages. The reason why it is perfect for this case is that Object-Oriented design allows for easy 1:1 mappings between real objects and their representations in code. Are you a retailer which specializes in shoes? Well, chances are your app is going to have a class Shoe and an instance of it is going to tell you everything you need to know about that specific shoe.

In this post, we’re going to take a look at building an inventory management app and not just the Flutter bit. This post includes a fully functioning Node.js backend as well — ensuring you successfully deliver your message and content to your users.

Note: This will not be a step-by-step guide, as that would be difficult to digest at a high level. Instead, we will look at each piece and break down the important components.

Inventory App Features We’ll be Building

Below is everything entailed in going from an unstarted project to a functioning application pulling real data. Here is what we will end up with:

inventory app example pulling real data

Let’s dive in!

Basis

We will use the example of a library — yes, the variety filled with a book! A library is essentially a warehouse filled with inventory (in this case, books). For many businesses an inventory application, at its core, would support browsing and tracking items. In the context of a library, those functionalities manifest themselves in the following ways:

  • Browsing
    • view the full catalog
    • search for a specific title
    • view information about a specific title
  • Tracking
    • see a title’s availability
    • check out a copy
    • return a copy

Our app will handle all of the above.

Setup

Database
MongoDB will be used to store the data. There is no special setup required, we just load all the items into a collection and later run the Node.js server on the same machine to leverage Mongo’s already exposed localhost connection.

Most likely, when building an app of this type, it will be used to access an existing dataset. The data in this example will be a subset of the most popular titles on Project Gutenberg supplemented with Wikipedia details.

Backend (API)
For our server, we will be using hapi with a few smaller dependencies like the official Node.js MongoDB drive and boom for error handling.

Once hapi is installed, we must create our startup file. This will get the server up and running to fulfill requests. Let’s use index.js.

'use strict';

const Hapi = require('hapi');
const routes = require('./routes');

const server = Hapi.server({
port: 3000
});

server.route(routes.allRoutes);

const init = async () => {

await server.start();
console.log(`Server running at: ${server.info.uri}`);
};

process.on('unhandledRejection', (err) => {

console.log(err);
process.exit(1);
});

init();

Tiny, right? Hapi requires very little boilerplate. The majority of this is ripped right from hapi’s Getting Started guide. Besides removing the host property on the server configuration object in order to fall-back to the machines hostname, the only custom line is as follows:

server.route(routes.allRoutes);

 

This line imports and registers all of the endpoints we define in our second, and final, file: routes.js. We separate these so that the server configuration doesn’t get drowned out by the much larger endpoint definitions. In a more complex app, we would likely want multiple files which logically group endpoints into smaller buckets. Here is routes.js with an example endpoint. Its only job is to export an array of configuration objects.

const Boom = require('boom');

exports.allRoutes = [
  {
    method: 'GET',
    path: '/',
    handler: async (request, h) => {
      return 'Hello world';
    }
  }
];

 

Flutter
Enter Flutter! When creating a new Flutter project through IntelliJ, a main.dart file is created for a basic sample app which implements a counter. This is helpful when learning, but we need to rip out some of that starter code. Here is a single page app which we can use as a starting point.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Library',
      theme: ThemeData(
        primarySwatch: Colors.deepOrange,
      ),
      home: CatalogPage(),
    );
  }
}

class CatalogPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Catalog'),
      ),
      body: Center(
        child: Text('List of books'),
      ),
    );
  }
}

 

flutterSetup

 

 

 

 

 

 

 

 

 

 

 

 

 

Catalog

Backend
Now that we’re set up, let’s start serving up data. We replace the example endpoint we defined before with one which returns the full list of books in the database.

const MongoClient = require('mongodb').MongoClient;
const Boom = require('boom');

exports.allRoutes = [
  {
    method: 'GET',
    path: '/bookList',
    handler: async (request, h) => {
      let client;
      try {
        client = await MongoClient.connect('mongodb://localhost:27017');
        let books = client.db('inventory').collection('books');

        // fetch all books
        return await books.find({},
          {
            projection : {
              _id: 0,
              id: 1,
              title: 1,
              authors : 1,
            }
          }
        ).toArray();
      } catch (e) {
        console.error(e.message);
        return Boom.internal(e);
      } finally {
        if (client && client.close){
          client.close();
        }
      }
    }
  }
];

 

You may notice async/await syntax. As it is available in both recent Node.js versions and Dart, we will use it throughout the backend and the app.

There isn’t too much going on here. We connect to MongoDB, specifically the collection books in the database inventory, and run a find query with an empty filter object (first argument) so that all records are pulled. For cleanliness of data, we project only the properties of a book which we would be interested in when listing them en masse.

Flutter
The first thing we need to define is the representation of a book. We will go ahead and include all fields we need to be known for a book, even though only a few of them will be populated from the results of the /bookList endpoint.

class Book {
  final String id;
  final String title;
  final List<String> authors;
  final String releaseDate;
  final String description;
  final int totalCopies;
  final int availableCopies;

  /// Creates a Book instance out of JSON received from the API.
  Book.fromJson(Map<String, dynamic> json)
      : id = json['id'],
        title = json['title'],
        releaseDate = json['releaseDate'],
        description = json['description'],
        totalCopies = json['totalCopies'],
        availableCopies = json['availableCopies'],
        authors = json['authors'].retype<String>();
}

 

We will use the “Serializing JSON inside model classes” strategy shown in Flutter’s JSON and serialization guide.

CatalogPage is a Stateless widget, because the full screen including the appbar doesn’t need to be re-rendered in the future, just the content. For that, we create a Stateful Widget, called CatalogList, which we will place in the body of CatalogPage. To keep this example concise, network requests will be made directly from widgets. It is better to practice to split them out into a non-UI library. Here is Catalog with basic display functionality complete followed by a breakdown below.

/// The list of books.
class CatalogList extends StatefulWidget {
  @override
  _CatalogListState createState() => _CatalogListState();
}

class _CatalogListState extends State<CatalogList> {
  /// All books in the catalog.
  List<Book> books;

  /// Books currently being displayed in the list.
  List<Book> displayedBooks;

  /// Kicks off API fetch on creation.
  _CatalogListState() {
    _fetchBookList();
  }

  /// Fetches the list of books and updates state.
  void _fetchBookList() async {
    http.Response response = await http.get('http://<API location>/bookList');
    List<Map<String, dynamic>> newBooksRaw =
        json.decode(response.body).retype<Map<String, dynamic>>();
    List<Book> newBooks =
        newBooksRaw.map((bookData) => Book.fromJson(bookData)).toList();
    setState(() {
      books = newBooks;
      displayedBooks = books;
    });
  }

  @override
  Widget build(BuildContext context) {
    return displayedBooks != null
        ? Column(
            children: <Widget>[
              new Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: ListView.builder(
                    itemBuilder: (BuildContext context, int index) => Card(
                          elevation: 2.0,
                          child: ListTile(
                            title: Text(
                              displayedBooks[index].title,
                              maxLines: 2,
                              overflow: TextOverflow.ellipsis,
                            ),
                            subtitle:
                                Text(displayedBooks[index].authors.join(' | ')),
                          ),
                        ),
                    itemCount: displayedBooks.length,
                  ),
                ),
              ),
            ],
          )
        : Center(child: CircularProgressIndicator());
  }
}

 

basicCatalog

 

 

 

 

 

 

 

 

 

 

 

 

 

Fetch Data
When the CatalogList widget is created, we immediately want to fetch the data on all books from the backend. We go ahead and create two list references, one for all the data downloaded and one for data currently displayed, as we know search functionality is coming and we won’t always be displaying the full catalog on screen. When data is first downloaded, though, these will be the same. We take advantage of the fromJSON serialization constructor we created to convert the backend’s JSON response into a list of formed Book objects in one list mapping call.

/// All books in the catalog.
List<Book> books;

/// Books currently being displayed in the list.
List<Book> displayedBooks;

/// Kicks off API fetch on creation.
_CatalogListState() {
  _fetchBookList();
}

/// Fetches the list of books and updates state.
void _fetchBookList() async {
  http.Response response = await http.get('http://<API location>/bookList');
  List<Map<String, dynamic>> newBooksRaw =
      json.decode(response.body).retype<Map<String, dynamic>>();
  List<Book> newBooks =
      newBooksRaw.map((bookData) => Book.fromJson(bookData)).toList();
  setState(() {
    books = newBooks;
    displayedBooks = books;
  });
}

 

Try/catch around the async body of _fetchBookList is omitted for readability. Make sure to catch possible exceptions/errors in production.

Build a List
Here is where we see Flutter start to shine. To convert this list of Book data into a rendered list on the screen, all we have to do is write an itemBuilder function which returns what a given item in the list will look like, then pass in the list of data and it’s length. We use a Material Design Card containing a ListTile- a prebuilt widget which displays a title and subtitle (and optionally additional inner widgets).

child: ListView.builder(
  itemBuilder: (BuildContext context, int index) => Card(
        elevation: 2.0,
        child: ListTile(
          title: Text(
            displayedBooks[index].title,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
          ),
          subtitle:
              Text(displayedBooks[index].authors.join(' | ')),
        ),
      ),
  itemCount: displayedBooks.length,
),

 

styledListItems

 

 

 

 

 

 

 

That’s all it takes to build a ListView which is ready for production. It will lazily render new rows as they are scrolled into view, gracefully handle changes to the list of Books, adapt scrolling behavior to OS, and perform fantastically.

Much of the above is styling as well. We could get something functional in half as many lines.

child: ListView.builder(
  itemBuilder: (BuildContext context, int index) => ListTile(
        title: Text(displayedBooks[index].title),
        subtitle:
            Text(displayedBooks[index].authors.join(' | ')),
      ),
  itemCount: displayedBooks.length,
),

 

unstyledListItems

 

 

 

 

 

Handle Loading State
When the Catalog List is created, the data list which our ListView is populated from, displayedBooks, is null.

List<Book> displayedBooks;

 

Once the data fetch is complete, that variable will point to a valid List.

setState(() {
  books = newBooks;
  displayedBooks = books;
});

 

Once this occurs, the ListView can start rendering rows. In the meantime, we need to render something different. If handling moments like these require a lot of development effort, it can feel counterproductive to tackle them right out of the gate while true functionality is still being worked out. This can lead to polish/UX tasks being put on the afterburner. With Flutter, it’s easy to handle loading during our first pass at the screen. We just use a ternary expression in the build function to describe an alternate visual while displayedBooks is still null.

@override
Widget build(BuildContext context) {
  return displayedBooks != null
      ? Column(
          // ...rest of widget hierarchy for loaded state
        )
      : Center(child: CircularProgressIndicator());
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Search
Supporting search requires two main changes.

  1. Add a search input field which fires an event when it changes
  2. Add a search function to filter the displayed list when the search event is fired

An input field which fires an event is achieved with the combination of TextField and TextEditingController

/// The controller to keep track of search field content and changes.
final TextEditingController searchController = TextEditingController();
child: TextField(
  decoration: InputDecoration(hintText: 'Search for titles...'),
  controller: searchController,
),

 

With the search bar in place, we can register a listener when the CatalogList is created to fire the function which will filter the full book list down to results to be displayed and update state. If the search text becomes empty, the list is set back to the full catalog.

/// Kicks off API fetch on creation.
_CatalogListState() {
  _fetchBookList();
  searchController.addListener(_search);
}
/// Performs a case insensitive search.
void _search() {
  if (searchController.text == '') {
    setState(() {
      displayedBooks = books;
    });
  } else {
    List<Book> filteredBooks = books
        .where((book) => book.title
            .toLowerCase()
            .contains(searchController.text.toLowerCase()))
        .toList();
    setState(() {
      displayedBooks = filteredBooks;
    });
  }
}

 

 

Navigation
The Catalog page is complete, and now we need to be able to take a deeper look at an individual item. The next page will be called DetailPage, so we’ll rig up each item in the list to move forward to the respective book’s details. Conveniently, ListTile has builtin touch handling, so we can just add a single property

onTap: () {
  Navigator.of(context).push(MaterialPageRoute(
      builder: (BuildContext context) {
    return DetailPage(displayedBooks[index].id);
  }));
}),

 

We only pass the id instead of the Book instance, since we will be fetching the book’s most up-to-date full data from the backend when loading DetailPage anyways.

Details

Backend
We add a new endpoint configuration object onto allRoutes to return full details of a title. It is nearly identical to the full listing except we switch to findOne, add a filter for an id passed from the app, and project additional fields. We also call out to a function to calculate the number of copies available, but we will wait to see that in the Tracking section.

{
  method: 'GET',
  path: '/book',
  handler: async (request, h) => {
    let client;
    try {
      client = await MongoClient.connect('mongodb://localhost:27017');
      let books = client.db('inventory').collection('books');

      // fetch matching book
      let book = await books.findOne(
        {
          id: request.query.id
        },
        {
          projection : {
            _id: 0,
            id: 1,
            title: 1,
            authors : 1,
            releaseDate : 1,
            description: 1,
            totalCopies: 1,
            checkedOutTo: 1
          }
        }
      );
      setAvailableCopies(book);
      return book;
    } catch (e) {
      console.error(e.message);
      return Boom.internal(e);
    } finally {
      if (client && client.close){
        client.close();
      }
    }
  }
},

 

 

Flutter
The basic DetailPage widget is a bit larger, but most of the functionality will look familiar from CatalogList. This time we make DetailPage itself the Stateful Widget, since we want to allow the AppBar to update with the book’s title once data is loaded.

/// The screen which displays the full details of a given book.
class DetailPage extends StatefulWidget {
  final String bookId;

  DetailPage(this.bookId);

  @override
  _DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  /// The full book data.
  Book book;

  /// Kicks off API fetch on creation.
  _DetailPageState() {
    _fetchBookDetails();
  }

  /// Fetches the books details and updates state.
  void _fetchBookDetails() async {
    http.Response response =
        await http.get('http://<API location>/book?id=${widget.bookId}');
    Map<String, dynamic> newBookRaw = json.decode(response.body);
    Book newBook = Book.fromJson(newBookRaw);
    setState(() {
      book = newBook;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(book?.title ?? ''),
      ),
      body: book != null
          ? new Center(
              child: new SingleChildScrollView(
                child: Padding(
                  padding: const EdgeInsets.all(20.0),
                  child: Card(
                    elevation: 5.0,
                    child: Center(
                        child: Padding(
                      padding: const EdgeInsets.fromLTRB(24.0, 0.0, 24.0, 24.0),
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                        crossAxisAlignment: CrossAxisAlignment.center,
                        children: <Widget>[
                          _BodySection('Author', book.authors.join('\n')),
                          _BodySection(
                              'Release Data', book.releaseDate ?? 'N/A'),
                          _BodySection('Description', book.description),
                        ],
                      ),
                    )),
                  ),
                ),
              ),
            )
          : Center(child: CircularProgressIndicator()),
    );
  }
}

class _BodySection extends StatelessWidget {
  final String title;
  final String content;

  _BodySection(this.title, this.content);

  @override
  Widget build(BuildContext context) {
    return new Padding(
      padding: const EdgeInsets.only(top: 24.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          Text(title, style: Theme.of(context).textTheme.title),
          Text(content, style: TextStyle(color: Colors.grey[700]))
        ],
      ),
    );
  }
}

 

We could have fetched all these details back on the CatalogPage and simply passed them through the be displayed on the DetailPage. The reason we opt for pulling the data fresh is to ensure we have the very latest details on an item when we view it. In this example, it’s unlikely that any of the fields we are displaying would change frequently. In other industries, however, a DetailPage might be displaying volatile data. These considerations especially come into play when we add our next section.

Tracking

We will support two operations on books: checking out and returning. These operations will drive the number of copies available to be checked out by other users as well. When a user checks a book out, they are grabbing it from the library and marking one of the copies as taken by them. When they return a copy, they are again providing their name and stating that they have placed the copy back into the library. In this example, no unique identifiers are used for individual copies, however, it would be a small change to add an additional field to the app and modify the backend to accept a copy identifier (potentially a barcode).

The three fields in the database driving these interactions will be:

  • totalCopies – total number of copies of a title which the library has in circulation
  • checkedOutTo – array of names to which copies are checked out
    along with one virtual field calculated at request time
  • availableCopies – number of copies in the library available for checkout (total copies – checked out copies)

 

Backend

We add two endpoint configuration objects. /checkOutBook will push a name onto the checkedOutTo array, and /returnBook will splice a name out of it.

{
  method: 'POST',
  path: '/checkOutBook',
  handler: async (request, h) => {
    let client;
    try {
      client = await MongoClient.connect('mongodb://localhost:27017');
      let books = client.db('inventory').collection('books');

      // update book data with name added to check out list
      let bookResult = await books.findOneAndUpdate(
        {
          id: request.payload.id
        },
        {
          $push : { checkedOutTo : request.payload.name }
        },
        {
          projection : {
            _id: 0,
            id: 1,
            title: 1,
            authors : 1,
            releaseDate : 1,
            description: 1,
            totalCopies: 1,
            checkedOutTo: 1
          },
          returnOriginal : false
        }
      );
      let book = bookResult.value;
      setAvailableCopies(book);
      return book;
    } catch (e) {
      console.error(e.message);
      return Boom.internal(e);
    } finally {
      if (client && client.close){
        client.close();
      }
    }
  }
},
{
  method: 'POST',
  path: '/returnBook',
  handler: async (request, h) => {
    let client;
    try {
      client = await MongoClient.connect('mongodb://localhost:27017');
      let books = client.db('inventory').collection('books');

      // get current list of checked out copies
      let currentBookData = await books.findOne(
        {
          id: request.payload.id
        }
      );
      let nameIndex = currentBookData.checkedOutTo.indexOf(request.payload.name);
      if (nameIndex !== -1){
        currentBookData.checkedOutTo.splice(nameIndex, 1);
      }

      // update book data with name removed from check out list
      let bookResult = await books.findOneAndUpdate(
        {
          id: request.payload.id
        },
        {
          $set : { checkedOutTo : currentBookData.checkedOutTo }
        },
        {
          projection : {
            _id: 0,
            id: 1,
            title: 1,
            authors : 1,
            releaseDate : 1,
            description: 1,
            totalCopies: 1,
            checkedOutTo: 1
          },
          returnOriginal : false
        }
      );
      let book = bookResult.value;
      setAvailableCopies(book);
      return book;
    } catch (e) {
      console.error(e.message);
      return Boom.internal(e);
    } finally {
      if (client && client.close){
        client.close();
      }
    }
  }
}

 

We also add a quick helper method to routes.js to calculate available copies.

function setAvailableCopies(book){
  book.availableCopies = book.totalCopies - (book.checkedOutTo ? book.checkedOutTo.length : 0);
}

 

 

Flutter
Beneath the other body sections, we add availability details and a small form for checkout/return. Since both actions require the same info, they can share an input box.

_BodySection('Available Copies',
    '${book.availableCopies} / ${book.totalCopies}'),
Column(
  children: <Widget>[
    TextField(
      decoration:
          InputDecoration(hintText: 'Enter name'),
      controller: nameController,
    ),
    new Padding(
      padding: const EdgeInsets.only(top: 16.0),
      child: new Row(
        mainAxisAlignment:
            MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          RaisedButton(
            child: Text('Check Out!'),
            onPressed: fieldHasContent &&
                    book.availableCopies > 0
                ? _checkOut
                : null,
          ),
          RaisedButton(
            child: Text('Return'),
            onPressed:
                fieldHasContent ? _return : null,
          ),
        ],
      ),
    )
  ],
)

 

 

detailsDisableddetailsEnabled

 

These buttons call functions to pass the current book id and entered a name to the backend, and will update state with the new version of book details returned. This ensures that the available copy count will stay in sync with actions performed.

Each button is conditionally disabled by passing null to its onPressed property. Both buttons are disabled by the fieldHasContent flag which is set when the name input field is empty, and the check out button is additionally disabled when there are no available copies.

We have a functional, performant, aesthetically pleasing app with associated backend all in ~500 lines of code. Creating a solution for Asset/Inventory Management has never been easier, and Flutter continues to improve daily. Don’t let it’s “beta” tag fool you, Flutter is production ready. In fact, we just built a production Inventory Management application for Belden Brick, so that the brick distributors they work with can access browse their inventory, search products, view product images, order samples, and more!

Below you will find the full files from this demo.

Flutter: https://bitbucket.org/snippets/shockoe/7eMxEq
Hapi: https://bitbucket.org/snippets/shockoe/ne8jR6

 

 

Related post:

Three Reasons FLutter is a Viable Cross-Platform Framework
Google Flutter goes Beta at #MWC18

4 Creative Ways Mobile can Keep Your Supply Chain on Track

4 Creative Ways Mobile can Keep Your Supply Chain on Track

Mobile devices are everywhere. Every aspect of the supply chain, from the supplier to the consumer, has been impacted by mobile technology. There is a good chance that you are already using some type of mobile supply chain application, but is your mobile strategy keeping up with the needs of your operation?

Not only is mobile hardware changing quickly, with new phones being released constantly, but the software that runs on these devices is also changing. Device manufacturers are continuing to add more and more features into the operating system of mobile devices as well as giving developers the ability to quickly take advantage of these new features in their mobile apps. In addition to the devices and software improvements, manufacturers have also been producing rugged cases for mobile devices that add superior durability while still being able to take advantage of the latest mobile technology and features.

Barcode Scanners Increase App Adoptability 

While barcode scanners have been used with mobile apps for a very long time, there have also been many hardware and software improvements that have increased both reliability and ease of use. A good scanner can make the difference in the adoptability of your app and can make your business more efficient from source to customer. Reducing the time it takes to actually scan an item and adding support for many more barcode types can make tasks like inventory and asset management much more efficient. It complements voice features well by allowing identification of products in environments where speech recognition might not work well.

Related: See how we added product scanning to improve warehouse inventory count and identify packages at delivery checkpoints for AC Moore

Speech Recognition Tailored To Your Product Lines

Voice and speech recognition have probably have made very rapid advancements in the past few years. All the major players including Apple, Google, Amazon, Microsoft, and Samsung, have worked on optimizing and perfecting the ability to recognize and process speech. Not only will you find it on mobile devices, but this past year has seen a large adoption of devices such as Amazon’s stand-alone Echo and Dot products. What used to be a novelty years ago has now transformed into a valuable feature available on almost all modern mobile devices. Speech recognition is able to easily complement technologies like barcode scanning in areas such as inventory management. It also has the flexibility of being as adjustable and powerful as you need by tying a recognized vocabulary with data from suppliers, transportation, sales, and consumers.

  • Using voice to document quality control inspections
  • Improving safety by not requiring workers to take eyes off equipment and instead use voice to enter/read data

GPS

GPS technology has also improved over the years giving mobile devices much quicker and more precise location information. While traditional mobile uses of GPS like navigation are quite mainstream, features such as geofencing are not as widespread. Geofencing creates a virtual geographic boundary that mobile software can react to. It can trigger a check in when a truck arrives with a shipment or optimize and adjust workflows based on estimated times of arrival. When combined with a smart mobile app, it can also give receiving terminals detailed information about drivers and shipments (eliminating possible paperwork) as well as giving feedback back to drivers about possible gate changes or wait times.

Supply Chain Case Study: See how changes to truck routing and package tracking improved routing and fuel efficiency for JB Hunt.

Beacons

With the introduction of Bluetooth® Low Energy (BLE) beacons, companies have been able to improve the logistics of their business in a much more efficient (and affordable) way than what was delivered with RFID. Early adoption of beacons was often used for indoor navigation, where GPS signal was unreliable. Shockoe has tackled this through a great personalized tour experience app with Anheuser-Busch. Uses have expanded to allow workers to efficiently navigate and locate items in storage, as well as being able to give a customer the ability to find the product they are looking for in a retail store. In addition to location proximity, beacons can also be fitted with additional sensors for light, humidity, and temperature; which expands their many uses to areas such as storage and transportation. A beacons low cost, enhanced battery lifetime, and the fact that they can be used with mobile devices (bypassing the need for specialized equipment), makes them particularly ideal for supply chain operations.

  • Alert personnel that humidity levels for monitored goods are above acceptable range and direct them to the location using their mobile device
  • Navigate grocery customers looking for a certain product to the correct aisle for the store

Mobile technologies are being used in all aspects of supply chain management from manufacturing to the consumer. At Shockoe, we are building smart mobile apps that leverage the latest features available. We work to give suppliers, truck drivers, dispatchers, receivers, retail stores, and customers real-time insights and collaboration using an app they love. 

Related post: 4 Tips in Designing a Retail Inventory Management App