Build a cryptocurrency alert app using Kotlin and Go: Part 2 - The backend
You will need Android Studio 3+ and Go 1.10.2+ installed on your machine. You should have some familiarity with Android development and the Kotlin language.
In the first part of this article, we started building our service by creating our Android application. The application, however, requires a backend to work properly. So in this part, we will be creating the backend of the application.
We will be using Go to build the backend of the application. The framework in Go we will be using is Echo.
As a recap, here is a screen recording of what we will have built when we are done:
Prerequisites
To follow along, you need:
- To have completed part one of the article.
- Android Studio installed on your machine (v3.x or later). Download here.
- Go version 1.10.2 or later installed.
- SQLite installed on your machine.
- Basic knowledge on using the Android Studio IDE.
- Basic knowledge of Kotlin programming language. See the official docs.
- Basic knowledge of Go and the Echo framework.
Building our Go API
Setting up our project
To get started, create a new project directory for your application. We will create one called backend
. It is recommended that you create this in your $GOPATH
however, it is not a requirement.
In the project directory, create three new directories:
- database
- notification
- routes
In the database
directory, create a new directory called model
. In this database
directory, we will store all things related to the database including the SQLite database file, the model
, and database
package.
In the notification
directory, we will have a package that will contain everything needed to send push notifications to the devices.
Finally, in the routes
directory, we will have the routes
package where we have the logic for each HTTP request.
Now let’s start building the application.
Building our core application
Create a new main.go
file in the root of the project. In this file, we will be adding the core of the project. We will be setting up the routing, middleware, and database.
In the main.go
file, paste the following code:
<span class="hljs-comment">// File: ./main.go</span>
<span class="hljs-keyword">package</span> main
<span class="hljs-keyword">import</span> (
<span class="hljs-string">"./database"</span>
<span class="hljs-string">"./routes"</span>
<span class="hljs-string">"github.com/labstack/echo"</span>
<span class="hljs-string">"github.com/labstack/echo/middleware"</span>
)
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
db := database.Initialize(<span class="hljs-string">"./database/db.sqlite"</span>)
database.Migrate(db)
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET(<span class="hljs-string">"/fetch-values"</span>, routes.GetPrices())
e.POST(<span class="hljs-string">"/btc-pref"</span>, routes.SaveDeviceSettings(db))
e.POST(<span class="hljs-string">"/eth-pref"</span>, routes.SaveDeviceSettings(db))
e.GET(<span class="hljs-string">"/simulate"</span>, routes.SimulatePriceChanges(db))
e.Start(<span class="hljs-string">":9000"</span>)
}
In the code above, we first imported some packages that the Go script will need to work. Then we instantiate the database using the database
subpackage that we imported. Next, we run the migration on the db
instance. This will create the database table the application needs to run if it does not already exist.
Next, we create a new Echo instance e
. We then use the instance to register the Logger middleware and the Recover middleware.
Logger middleware logs the information about each HTTP request.
Recover middleware recovers from panics anywhere in the chain, prints stack trace and handles the control to the centralized HTTPErrorHandler.
We then register our routes and map a handler to them using the routes
package we imported. The routes are:
GET /fetch-values
- fetches the current prices of all the supported currencies and returns a JSON response.POST /btc-pref
- stores the minimum and maximum price BTC has to exceed for a device before receiving a notification and returns a JSON response.POST /eth-pref
- stores the minimum and maximum price ETH has to exceed for a device before receiving a notification and returns a JSON response.GET /simulate
- simulates prices changes in the supported currencies.
After the routes, we start the server on port 9000.
You can choose a different port if 9000 is in use, just remember to also change it in your
MainActivity.kt
file.
Now that we have the main.go
file, let’s pull in all the imports the script needs. Open your terminal and run the following commands:
$ go get github.com/labstack/echo
$ go get github.com/labstack/echo/middleware
This will pull in Echo package and the Echo Middleware package. For the other two packages, database
and routes
, we will create those manually. Let’s do that now.
Creating our internal Go packages
As mentioned earlier, we are going to create some internal packages to make the application a lot more modular so let’s start with the database
package.
In the database
directory, create a new init.go
file and paste the following code:
<span class="hljs-comment">// File: ./database/init.go</span>
<span class="hljs-keyword">package</span> database
<span class="hljs-keyword">import</span> (
<span class="hljs-string">"database/sql"</span>
_ <span class="hljs-string">"github.com/mattn/go-sqlite3"</span>
)
<span class="hljs-comment">// Initialize initialises the database</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Initialize</span><span class="hljs-params">(filepath <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">sql</span>.<span class="hljs-title">DB</span></span> {
db, err := sql.Open(<span class="hljs-string">"sqlite3"</span>, filepath)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> || db == <span class="hljs-literal">nil</span> {
<span class="hljs-built_in">panic</span>(<span class="hljs-string">"Error connecting to database"</span>)
}
<span class="hljs-keyword">return</span> db
}
<span class="hljs-comment">// Migrate migrates the database</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">Migrate</span><span class="hljs-params">(db *sql.DB)</span></span> {
sql := <span class="hljs-string">`
CREATE TABLE IF NOT EXISTS devices(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
uuid VARCHAR NOT NULL,
btc_min INTEGER,
btc_max INTEGER,
eth_min INTEGER,
eth_max INTEGER
);
`</span>
_, err := db.Exec(sql)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-built_in">panic</span>(err)
}
}
In the file above, we first import two packages, the database/sql
, which is inbuilt, and the mattn/go-sqlite3 package, which is an sqlite3 driver for Go using database/sql
. To pull that in open the terminal and run the command below:
$ go get github.com/mattn/go-sqlite3
Next, we created a function called Initialize
and in this function, we initialize our SQLite database. This will create a new database file if it does not exist, or use an existing one.
We also have a Migrate
function where we specify the SQL query to be run when the application is initialized. As seen from the query, we create the table devices
only if it does not already exist.
That’s all for the init.go
file.
Create a new routes.go
file in the routes
directory and paste the following code:
<span class="hljs-comment">// File: ./routes/routes.go</span>
<span class="hljs-keyword">package</span> routes
<span class="hljs-keyword">import</span> (
<span class="hljs-string">"database/sql"</span>
<span class="hljs-string">"errors"</span>
<span class="hljs-string">"net/http"</span>
<span class="hljs-string">"strconv"</span>
<span class="hljs-string">"../database/model"</span>
<span class="hljs-string">"github.com/labstack/echo"</span>
)
Now let’s start defining the route handlers as used in the main.go
file.
First, we will add the GetPrices
function. In the same file paste the following code at the bottom:
<span class="hljs-comment">// GetPrices returns the coin prices</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetPrices</span><span class="hljs-params">()</span> <span class="hljs-title">echo</span>.<span class="hljs-title">HandlerFunc</span></span> {
<span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c echo.Context)</span> <span class="hljs-title">error</span></span> {
prices, err := model.GetCoinPrices(<span class="hljs-literal">true</span>)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> c.JSON(http.StatusBadGateway, err)
}
<span class="hljs-keyword">return</span> c.JSON(http.StatusOK, prices)
}
}
The function above is straightforward. We just get the prices from the model.GetCoinPrices
function and return them as a JSON response.
Note that we passed a boolean to the GetCoinPrices
function. This boolean is to mark whether to simulate the prices or fetch from the API directly. Since we are testing, we want to simulate the prices so it changes often.
The next function to add to the routes.go
file is the SaveDeviceSettings
function. In the same file, paste the following code to the bottom of the file:
<span class="hljs-keyword">var</span> postedSettings <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">formValue</span><span class="hljs-params">(c echo.Context, key <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(<span class="hljs-keyword">string</span>, error)</span></span> {
<span class="hljs-keyword">if</span> postedSettings == <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">if</span> err := c.Bind(&postedSettings); err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> <span class="hljs-string">""</span>, err
}
}
<span class="hljs-keyword">return</span> postedSettings[key], <span class="hljs-literal">nil</span>
}
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getCoinValueFromRequest</span><span class="hljs-params">(key <span class="hljs-keyword">string</span>, c echo.Context)</span> <span class="hljs-params">(<span class="hljs-keyword">int64</span>, error)</span></span> {
value, _ := formValue(c, key)
<span class="hljs-keyword">if</span> value != <span class="hljs-string">""</span> {
setting, err := strconv.ParseInt(value, <span class="hljs-number">10</span>, <span class="hljs-number">64</span>)
<span class="hljs-keyword">if</span> err == <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> setting, <span class="hljs-literal">nil</span>
}
}
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>, errors.New(<span class="hljs-string">"Invalid or empty key for: "</span> + key)
}
<span class="hljs-comment">// SaveDeviceSettings saves the device settings</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">SaveDeviceSettings</span><span class="hljs-params">(db *sql.DB)</span> <span class="hljs-title">echo</span>.<span class="hljs-title">HandlerFunc</span></span> {
<span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c echo.Context)</span> <span class="hljs-title">error</span></span> {
uuid, _ := formValue(c, <span class="hljs-string">"uuid"</span>)
field := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">int64</span>)
<span class="hljs-keyword">if</span> btcmin, err := getCoinValueFromRequest(<span class="hljs-string">"minBTC"</span>, c); err == <span class="hljs-literal">nil</span> {
field[<span class="hljs-string">"btc_min"</span>] = btcmin
}
<span class="hljs-keyword">if</span> btcmax, err := getCoinValueFromRequest(<span class="hljs-string">"maxBTC"</span>, c); err == <span class="hljs-literal">nil</span> {
field[<span class="hljs-string">"btc_max"</span>] = btcmax
}
<span class="hljs-keyword">if</span> ethmin, err := getCoinValueFromRequest(<span class="hljs-string">"minETH"</span>, c); err == <span class="hljs-literal">nil</span> {
field[<span class="hljs-string">"eth_min"</span>] = ethmin
}
<span class="hljs-keyword">if</span> ethmax, err := getCoinValueFromRequest(<span class="hljs-string">"maxETH"</span>, c); err == <span class="hljs-literal">nil</span> {
field[<span class="hljs-string">"eth_max"</span>] = ethmax
}
<span class="hljs-keyword">defer</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> { postedSettings = <span class="hljs-literal">nil</span> }()
device, err := model.SaveSettings(db, uuid, field)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> c.JSON(http.StatusBadRequest, err)
}
<span class="hljs-keyword">return</span> c.JSON(http.StatusOK, device)
}
}
In the code above, we have three functions. The first two are helper functions. We need them to get the posted form values from the request.
In the SaveDeviceSettings
function, we get the uuid
for the device, and conditionally get the minimum and maximum values for the coin. We save the values to the database using the model.SaveSettings
function and return a JSON response.
The final function to add will be the Simulate
function. Add the following code to the bottom of the file:
<span class="hljs-comment">// SimulatePriceChanges simulates the prices changes</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">SimulatePriceChanges</span><span class="hljs-params">(db *sql.DB)</span> <span class="hljs-title">echo</span>.<span class="hljs-title">HandlerFunc</span></span> {
<span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c echo.Context)</span> <span class="hljs-title">error</span></span> {
prices, err := model.GetCoinPrices(<span class="hljs-literal">true</span>)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-built_in">panic</span>(err)
}
devices, err := model.NotifyDevicesOfPriceChange(db, prices)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-built_in">panic</span>(err)
}
resp := <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{
<span class="hljs-string">"prices"</span>: prices,
<span class="hljs-string">"devices"</span>: devices,
<span class="hljs-string">"status"</span>: <span class="hljs-string">"success"</span>,
}
<span class="hljs-keyword">return</span> c.JSON(http.StatusOK, resp)
}
}
In the function above, we fetch the prices for the coins, we then send that to the model.NotifyDevicesOfPriceChange
function, which finds devices with matching criteria and sends them a push notification. We then return a JSON response of the prices
, devices
and status
.
That’s all for the routes.
Lastly, let’s define the model. Create a new models.go
file in the database/model
directory and paste the following code:
<span class="hljs-comment">// File: ./database/model/models.go</span>
<span class="hljs-keyword">package</span> model
<span class="hljs-keyword">import</span> (
<span class="hljs-string">"database/sql"</span>
<span class="hljs-string">"encoding/json"</span>
<span class="hljs-string">"fmt"</span>
<span class="hljs-string">"io/ioutil"</span>
<span class="hljs-string">"math/big"</span>
<span class="hljs-string">"math/rand"</span>
<span class="hljs-string">"net/http"</span>
<span class="hljs-string">"time"</span>
<span class="hljs-string">"errors"</span>
<span class="hljs-string">"../../notification"</span>
)
Next, let’s define the structs for our object resources. In the same file, paste the following to the bottom:
<span class="hljs-comment">// CoinPrice represents a single coin resource</span>
<span class="hljs-keyword">type</span> CoinPrice <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}
<span class="hljs-comment">// Device represents a single device resource</span>
<span class="hljs-keyword">type</span> Device <span class="hljs-keyword">struct</span> {
ID <span class="hljs-keyword">int64</span> <span class="hljs-string">`json:"id"`</span>
UUID <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"uuid"`</span>
BTCMin <span class="hljs-keyword">int64</span> <span class="hljs-string">`json:"btc_min"`</span>
BTCMax <span class="hljs-keyword">int64</span> <span class="hljs-string">`json:"btc_max"`</span>
ETHMin <span class="hljs-keyword">int64</span> <span class="hljs-string">`json:"eth_min"`</span>
ETHMax <span class="hljs-keyword">int64</span> <span class="hljs-string">`json:"eth_max"`</span>
}
<span class="hljs-comment">// Devices represents a collection of Devices</span>
<span class="hljs-keyword">type</span> Devices <span class="hljs-keyword">struct</span> {
Devices []Device <span class="hljs-string">`json:"items"`</span>
}
Above, we have the CoinPrice
map. This will be used to handle the response from the API we will be using for our application. When a response from the API is gotten, we bind it to the CoinPrice
map.
The next one is the Device
struct. This represents the device resource. It matches the SQL schema of the table we created earlier in the article. When we want to create a new device resource to store in the database or retrieve one, we will use the Device
struct.
Finally, we have the Devices
struct which is simply a collection of multiple Device
structs. We use this if we want to return a collection of Device
s.
Go does not allow underscores in the struct names, so we will use the
json:``"``key_name``"
format to automatically convert to and from properties with the keys specified.
Let’s start defining our model functions.
In the same file, paste the following code to the bottom of the page:
<span class="hljs-comment">// CreateSettings creates a new device and saves it to the db</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">CreateSettings</span><span class="hljs-params">(db *sql.DB, uuid <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(Device, error)</span></span> {
device := Device{UUID: uuid, BTCMin: <span class="hljs-number">0</span>, BTCMax: <span class="hljs-number">0</span>, ETHMin: <span class="hljs-number">0</span>, ETHMax: <span class="hljs-number">0</span>}
stmt, err := db.Prepare(<span class="hljs-string">"INSERT INTO devices (uuid, btc_min, btc_max, eth_min, eth_max) VALUES (?, ?, ?, ?, ?)"</span>)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> device, err
}
res, err := stmt.Exec(device.UUID, device.BTCMin, device.BTCMax, device.ETHMin, device.ETHMax)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> device, err
}
lastID, err := res.LastInsertId()
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> device, err
}
device.ID = lastID
<span class="hljs-keyword">return</span> device, <span class="hljs-literal">nil</span>
}
The function above is used to create settings for a new device. In the function, a new device is created using the Device
struct. We then write the SQL query we want to use to create a new device.
We run Exec
on the SQL query to execute the query. If there’s no error, we get the last inserted ID from the query and assign that to the Device
struct we created earlier. We then return the created Device
.
Let’s add the next function. In the same file, paste the following code to the bottom:
<span class="hljs-comment">// GetSettings fetches the settings for a single user from the db</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetSettings</span><span class="hljs-params">(db *sql.DB, uuid <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(Device, error)</span></span> {
device := Device{}
<span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(uuid) <= <span class="hljs-number">0</span> {
<span class="hljs-keyword">return</span> device, errors.New(<span class="hljs-string">"Invalid device UUID"</span>)
}
err := db.QueryRow(<span class="hljs-string">"SELECT * FROM devices WHERE uuid=?"</span>, uuid).Scan(
&device.ID,
&device.UUID,
&device.BTCMin,
&device.BTCMax,
&device.ETHMin,
&device.ETHMax)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> CreateSettings(db, uuid)
}
<span class="hljs-keyword">return</span> device, <span class="hljs-literal">nil</span>
}
In the GetSettings
function above, we create an empty Device
struct. We run the query to fetch a device from the devices
table that matches the uuid
. We then use the Scan
method of the database package to save the row values to the Device
Instance.
If no device is found, a new one is created using the CreateSettings
function we created earlier, else the device found is returned.
Let’s add the next function. In the same file, paste the following code to the bottom:
<span class="hljs-comment">// SaveSettings saves the devices settings</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">SaveSettings</span><span class="hljs-params">(db *sql.DB, uuid <span class="hljs-keyword">string</span>, field <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">int64</span>)</span> <span class="hljs-params">(Device, error)</span></span> {
device, err := GetSettings(db, uuid)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> Device{}, err
}
<span class="hljs-keyword">if</span> btcmin, isset := field[<span class="hljs-string">"btc_min"</span>]; isset {
device.BTCMin = btcmin
}
<span class="hljs-keyword">if</span> btcmax, isset := field[<span class="hljs-string">"btc_max"</span>]; isset {
device.BTCMax = btcmax
}
<span class="hljs-keyword">if</span> ethmin, isset := field[<span class="hljs-string">"eth_min"</span>]; isset {
device.ETHMin = ethmin
}
<span class="hljs-keyword">if</span> ethmax, isset := field[<span class="hljs-string">"eth_max"</span>]; isset {
device.ETHMax = ethmax
}
stmt, err := db.Prepare(<span class="hljs-string">"UPDATE devices SET btc_min = ?, btc_max = ?, eth_min = ?, eth_max = ? WHERE uuid = ?"</span>)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> Device{}, err
}
_, err = stmt.Exec(device.BTCMin, device.BTCMax, device.ETHMin, device.ETHMax, device.UUID)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> Device{}, err
}
<span class="hljs-keyword">return</span> device, <span class="hljs-literal">nil</span>
}
In the SaveSettings
function above, we get the existing settings using the GetSettings
function and then we conditionally update the existing value. We then write an SQL query to update the database with the new values. After this, we return the Device
struct.
Let’s add the next function. In the same file, paste the following code to the bottom:
<span class="hljs-comment">// GetCoinPrices gets the current coin prices</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetCoinPrices</span><span class="hljs-params">(simulate <span class="hljs-keyword">bool</span>)</span> <span class="hljs-params">(CoinPrice, error)</span></span> {
coinPrice := <span class="hljs-built_in">make</span>(CoinPrice)
currencies := [<span class="hljs-number">2</span>]<span class="hljs-keyword">string</span>{<span class="hljs-string">"ETH"</span>, <span class="hljs-string">"BTC"</span>}
<span class="hljs-keyword">for</span> _, curr := <span class="hljs-keyword">range</span> currencies {
<span class="hljs-keyword">if</span> simulate == <span class="hljs-literal">true</span> {
min := <span class="hljs-number">1000.0</span>
max := <span class="hljs-number">15000.0</span>
price, _ := big.NewFloat(min + rand.Float64()*(max-min)).SetPrec(<span class="hljs-number">8</span>).Float64()
coinPrice[curr] = <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{<span class="hljs-string">"USD"</span>: price}
<span class="hljs-keyword">continue</span>
}
url := fmt.Sprintf(<span class="hljs-string">"https://min-api.cryptocompare.com/data/pricehistorical?fsym=%s&tsyms=USD&ts=%d"</span>, curr, time.Now().Unix())
res, err := http.Get(url)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> coinPrice, err
}
<span class="hljs-keyword">defer</span> res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> coinPrice, err
}
<span class="hljs-keyword">var</span> f <span class="hljs-keyword">interface</span>{}
err = json.Unmarshal([]<span class="hljs-keyword">byte</span>(body), &f)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> coinPrice, err
}
priceMap := f.(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{})[curr]
<span class="hljs-keyword">for</span> _, price := <span class="hljs-keyword">range</span> priceMap.(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}) {
coinPrice[curr] = <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{<span class="hljs-string">"USD"</span>: price.(<span class="hljs-keyword">float64</span>)}
}
}
<span class="hljs-keyword">return</span> coinPrice, <span class="hljs-literal">nil</span>
}
In the function above, we create a new instance of coinPrice
and then we create an array of the two currencies we want to fetch, ETH and BTC. We then loop through the currencies and if simulate
is true
, we just return the simulated prices for the coins. If it’s false
, then for each of the currencies we do the following:
- Fetch the price for the currency from the API.
- Add the price of the currency to the
coinPrice
map.
After we are done, we return the prices.
The next and final function we want to add is the NotifyDevicesOfPriceChange
function. This is responsible for getting devices that match the minimum and maximum threshold and sending push notifications to them.
In the same file, paste the following code:
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">minMaxQuery</span><span class="hljs-params">(curr <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">string</span></span> {
<span class="hljs-keyword">return</span> <span class="hljs-string">`(`</span> + curr + <span class="hljs-string">`_min > 0 AND `</span> + curr + <span class="hljs-string">`_min > ?) OR (`</span> + curr + <span class="hljs-string">`_max > 0 AND `</span> + curr + <span class="hljs-string">`_max < ?)`</span>
}
<span class="hljs-comment">// NotifyDevicesOfPriceChange returns the devices that are within the range</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">NotifyDevicesOfPriceChange</span><span class="hljs-params">(db *sql.DB, prices CoinPrice)</span> <span class="hljs-params">(Devices, error)</span></span> {
devices := Devices{}
<span class="hljs-keyword">for</span> currency, price := <span class="hljs-keyword">range</span> prices {
pricing := price.(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{})
rows, err := db.Query(<span class="hljs-string">"SELECT * FROM devices WHERE "</span>+minMaxQuery(currency), pricing[<span class="hljs-string">"USD"</span>], pricing[<span class="hljs-string">"USD"</span>])
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> devices, err
}
<span class="hljs-keyword">defer</span> rows.Close()
<span class="hljs-keyword">for</span> rows.Next() {
device := Device{}
err = rows.Scan(&device.ID, &device.UUID, &device.BTCMin, &device.BTCMax, &device.ETHMin, &device.ETHMax)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> devices, err
}
devices.Devices = <span class="hljs-built_in">append</span>(devices.Devices, device)
notification.SendNotification(currency, pricing[<span class="hljs-string">"USD"</span>].(<span class="hljs-keyword">float64</span>), device.UUID)
}
}
<span class="hljs-keyword">return</span> devices, <span class="hljs-literal">nil</span>
}
In the code above we have two functions, the first is minMaxQuery
which is a helper function that helps us generate the SQL query for the min and max of a currency.
The second function is the NotifyDevicesOfPriceChange
function. In here we loop through the currency prices and for each of the price we check the database for devices that match the minimum and maximum prices.
When we have the devices, we loop through them and send a push notification using the notification.SendNotification
method. We then return the devices we sent the notification to.
That’s all for the model package. We have one last package to add and that's the notification
package. We used it in the code above to send push notification so let’s define it.
In the notifications
directory, create a push.go
file and paste the following code:
<span class="hljs-comment">// File: ./notification/push.go</span>
<span class="hljs-keyword">package</span> notification
<span class="hljs-keyword">import</span> (
<span class="hljs-string">"fmt"</span>
<span class="hljs-string">"strconv"</span>
<span class="hljs-string">"github.com/pusher/push-notifications-go"</span>
)
<span class="hljs-keyword">const</span> (
instanceID = <span class="hljs-string">"PUSHER_BEAMS_INSTANCE_ID"</span>
secretKey = <span class="hljs-string">"PUSHER_BEAMS_SECRET_KEY"</span>
)
<span class="hljs-comment">// SendNotification sends push notification to devices</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">SendNotification</span><span class="hljs-params">(currency <span class="hljs-keyword">string</span>, price <span class="hljs-keyword">float64</span>, uuid <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
notifications, err := pushnotifications.New(instanceID, secretKey)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> err
}
publishRequest := <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{
<span class="hljs-string">"fcm"</span>: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{
<span class="hljs-string">"notification"</span>: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}{
<span class="hljs-string">"title"</span>: currency + <span class="hljs-string">" Price Change"</span>,
<span class="hljs-string">"body"</span>: fmt.Sprintf(<span class="hljs-string">"The price of %s has changed to $%s"</span>, currency, strconv.FormatFloat(price, <span class="hljs-string">'f'</span>, <span class="hljs-number">2</span>, <span class="hljs-number">64</span>)),
},
},
}
interest := fmt.Sprintf(<span class="hljs-string">"%s_%s_changed"</span>, uuid, currency)
_, err = notifications.Publish([]<span class="hljs-keyword">string</span>{interest}, publishRequest)
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
<span class="hljs-keyword">return</span> err
}
<span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
Replace the
PUSHER_BEAMS_*
key with the credentials in your Pusher dashboard.
In the code above, we have the SendNotification
function. In there we instantiate a new Pusher Beams instance using the InstanceID
and secretKey
defined above the function.
We then create a publishRequest
variable which contains the Android notification payload. This payload is what we will send to the Pusher Beams backend and will contain everything needed to send the notification to the Android device.
Next, we create an interest
variable which will be the interest we want to push the notification to. The format of the interest will match the one we subscribed to in part one of this tutorial. Next, we call the Publish
function of the Pusher Beams package to send the notification to the device.
One final thing we need to do is pull the Pusher Beams package into our $GOPATH
. Open your terminal and run the following command:
$ go get github.com/pusher/push-notifications-go
When the command has executed successfully, we can now run the application.
Running our application
Now that we have finished building the application, we need to run both the backend and the Android application.
Open your terminal and execute the following command from the root of the project to run the Go application:
$ go run main.go
This should start the server on port 9000.
Next, go to Android Studio and launch your Android project. At this point, you can now see the application. You can go ahead to set the minimum and maximum limits for both the BTC and ETH currency.
Now minimize the application in your simulator and open the notification center. Visit the URL http://localhost:9000/simulate to simulate the currency changes. You should see the notifications come into the device as shown below:
Conclusion
In this article, we have been able to see how you can create a cryptocurrency watcher application for Android using Pusher Beams and Go. This tutorial is available for iOS also here.
The source code to the application built in this article is available on GitHub.
This post first appeared on the Pusher blog.