Creating a Real-Time Cordova App with Vue, Vuetify and Butterfly Server .NET

In my last post, I walked through the steps to create a real-time web app to manage a todo list using Butterfly Server .NET.

In this post, we’ll reuse the same server but create a Cordova app that can be installed and run on Android and iOS to access our todo list in real-time.

Just Let Me Try It

Prefer to skip the step-by-step instructions?

Run this in a terminal or command prompt…

git clone https://github.com/firesharkstudios/butterfly-server-dotnet
cd butterfly-server-dotnet\Butterfly.Example.Todo
dotnet run -vm

Run this in a second terminal or command prompt…#

# This assumes you have Cordova and Android Studio installed
cd butterfly-server-dotnet\Butterfly.Example.Todo\cordova
npm install
# In both config.xml and src\main.js, replace each localhost:8000
# with <your DHCP assigned IP address>:8000 (like 192.168.1.15:8000)
npm run build
cordova platform add android
# Open Android Studio
# Click Tools, AVD Manager
# Startup the desired Android emulator
cordova run android

Or follow the instructions below to build your Cordova app from scratch.

Building the Cordova App

This assumes you have Cordova and Android Studio already installed.

The localhost reference does not appear to work properly within the Android emulator to access our local Butterfly Server .NET; therefore, be sure to replace localhost references with your local IP address as noted below.

Let’s again use npm to create our client and add the Android platform to our Cordova project…

npm install -g vue-cli
# Just accept the defaults...
vue init vuetifyjs/cordova my-todo-cordova-client
cd my-todo-cordova-client
npm install
npm install butterfly-client reqwest
cordova platform add android

The above commands…

Next, let’s edit config.xml to allow our Cordova client to access our local Butterfly Server .NET

<!--
Replace 'localhost' with your DHCP assigned address
(localhost and 127.0.0.1 do not work in Android emulator)
-->
<allow-intent href="http://localhost:8000/*" />
<allow-intent href="ws://localhost:8000/*" />

Next, edit src/main.js to add a couple of imports…

import { ArrayDataEventHandler, WebSocketChannelClient } from 'butterfly-client'
import reqwest from 'reqwest'

Next, edit src/main.js to replace the existing new Vue() call with…

new Vue({
el: '#app',
router,
template: '<App/>',
components: { App },
head: {
meta: [
{
name: 'viewport',
content: 'width=device-width, initial-scale=1, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
}
]
},
data() {
return {
channelClient: null,
channelClientState: null,
}
},
methods: {
// Replace 'localhost' with your DHCP assigned address
// (localhost and 127.0.0.1 do not work in Android emulator)
callApi(url, rawData) {
let fullUrl = `http://localhost:8000${url}`;
console.debug(`callApi():url=${url},fullUrl=${fullUrl}`);
return reqwest({
url: fullUrl,
method: 'POST',
data: JSON.stringify(rawData),
});
},
subscribe(options) {
let self = this;
self.channelClient.subscribe({
channel: options.key,
vars: options.vars,
handler: new ArrayDataEventHandler({
arrayMapping: options.arrayMapping,
onInitialEnd: options.onInitialEnd,
onChannelMessage: options.onChannelMessage
}),
});
},
unsubscribe(key) {
let self = this;
self.channelClient.unsubscribe(key);
},
},
beforeMount() {
let self = this;
    // Replace 'localhost' with your DHCP assigned address
// (localhost and 127.0.0.1 do not work in Android emulator)
let url = `ws://localhost:8000/ws`;
self.channelClient = new WebSocketChannelClient({
url,
onStateChange(value) {
self.channelClientState = value;
}
});
self.channelClient.connect();
},
})

The above code…

  • Creates a WebSocketChannelClient instance that maintains a WebSocket connection to our Butterfly Server .NET
  • Defines a callApi() method that our client can use to invoke API calls
  • Defines subscribe() and unsubscribe() methods that our client can use to subscribe/unsubscribe to specific channels on our Butterfly Server .NET

Next, edit src/App.vue to contain (identical to App.vue from the prior post)…

<template>
<v-app>
<v-content>
<v-toolbar>
<v-toolbar-title>My Todo Example</v-toolbar-title>
<v-spacer />
</v-toolbar>
<router-view v-if="$root.channelClientState=='Connected'"/>
<div class="px-5 py-5 text-xs-center" v-else>
<v-progress-circular indeterminate color="primary"/>
<span class="pl-2 title">
{{ $root.channelClientState }}...
</span>
</div>
</v-content>
</v-app>
</template>

The above template will cause the main content of our page to show a loading indicator until our WebSocketChannelClient has successfully connected to our Butterfly Server .NET.

Next, edit src/components/Hello.vue (identical to HelloWorld.vue from the prior post) to contain…

<template>
<v-container fluid>
<Todos/>
</v-container>
</template>
<script>
import Todos from '@/components/Todos'
export default {
components: {
Todos,
}
}
</script>

Next, create a new src/components/Todos.vue (identical to Todos.vue from the prior post) which contains…

<template>
<div>
<div class="px-3 py-3 text-xs-center" v-if="items.length==0">
No todos yet
</div>
<v-list v-else>
<Todo v-for="item in items" :key="item.id" :item="item" @remove="remove" />
</v-list>
<div class="px-3 py-3 text-xs-center">
<v-btn color="primary" @click="add">Add Todo</v-btn>
</div>
</div>
</template>
<script>
import Todo from '@/components/Todo'
export default {
components: {
Todo,
},
data () {
return {
items: [],
}
},
methods: {
add() {
this.$root.callApi('/api/todo/insert', {
name: 'A new todo item',
});
},
remove(id) {
this.$root.callApi('/api/todo/delete', id);
},
},
mounted() {
let self = this;
self.$root.subscribe({
arrayMapping: {
todo: self.items,
},
key: 'todos',
});
}
}
</script>

The above Todos.vue is responsible for…

Next, create a new src/components/Todo.vue (identical to Todo.vue from the prior post) which contains…

<template>
<v-list-tile>
<v-list-tile-content>
<v-list-tile-title>{{ item.name }}</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-btn icon @click="$emit('remove', item.id)">
<v-icon>delete</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</template>
<script>
export default {
props: {
item: null
}
}
</script>

The above Todo.vue is responsible for rendering a single todo.

Finally, disable eslint by commenting out this section in build/webpack.base.conf.js

/*
{
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
*/

Trying It

The end result will look something like this depending on the Android emulator you selected…

Before trying our Cordova app…

  • Run the server within Visual Studio (either create the server per the last post or clone from GitHub and run Butterfly.Example.Todo.Server)
  • Open Android Studio, click Tools, AVD Manager, and starting the desired Android emulator

Now, run in the my-todo-cordova-client directory…

npm run build
cordova run android

This will install and launch your Cordova app within the Android emulator you already have running.

Didn’t work? Be sure to double check you replaced both localhost references in src/main.js and config.xml with your local IP address.

Next Steps

The Butterfly Server .NET supports building much more sophisticated real-time web apps (can subscribe to channels with multiple datasets, can join multiple tables in each dataset, etc). See GitHub for more details.