Problem
I've the below simple go server that is running at my laptop (Mac/Windows/Linux):
package main
import (
"fmt"
"log"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
log.Println(http.ListenAndServe("localhost:6060", nil))
}
Can I use the same codebase to run my app at mobile webview
, without using gomobile or other packages, so I've my code as universal app?
Solution
The answer is "Yes", but some slight modification to the file itself is required.
- Remove everything from the
func main() {}
as we'll build the final result as a shared library, not as an executable binary. - Run the server in an
//export
function. - Run the server from an
anonymous goroutine
asgo func() {}()
so it is not blocking the main thread of the mobile app. - To keep the server gorotine running, we need to use a chanel as
<-c
to prevent the gorotine from exit. - Use
cgo
by addingimport "C"
, so the main file become like this:
package main
import "C"
// other imports should be seperate from the special Cgo import
import (
"fmt"
"log"
"net/http"
)
//export server
func server() {
c := make(chan bool)
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
<-c
}()
http.HandleFunc("/", handler)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there %s!", r.URL.Path[1:])
}
func main() {}
- Ensure to have Androd
NDK
installed, and you know its bath - Build the
c-shared
output with an output name aslibxxx
, to build forAndroid
use:
CGO_ENABLED=1 \
GOOS=android \
GOARCH=arm \
GOARM=7 \
CC=$(NDK_BIN)/armv7a-linux-androideabi21-clang \
go build -buildmode=c-shared -o libfoo.so http.go
Wait As android has multiple architectures, we need to compile for each one individually, so we can have all the process automated in a Makefile
as below after creating the android app by selecting Native C++
from the project templates, below the output library name is libfoo
and 2 files will be generated in each folder libfoo.so
and libfoo.h
:
#Filename: Makefile
# To compile run:
# make android
IOS_OUT=lib/ios
ANDROID_OUT=../android_app/app/src/main/jniLibs
ANDROID_SDK=$(HOME)/Library/Android/sdk
NDK_BIN=$(ANDROID_SDK)/ndk/23.0.7599858/toolchains/llvm/prebuilt/darwin-x86_64/bin
android-armv7a:
CGO_ENABLED=1 \
GOOS=android \
GOARCH=arm \
GOARM=7 \
CC=$(NDK_BIN)/armv7a-linux-androideabi21-clang \
go build -buildmode=c-shared -o $(ANDROID_OUT)/armeabi-v7a/libfoo.so ./cmd/libfoo
android-arm64:
CGO_ENABLED=1 \
GOOS=android \
GOARCH=arm64 \
CC=$(NDK_BIN)/aarch64-linux-android21-clang \
go build -buildmode=c-shared -o $(ANDROID_OUT)/arm64-v8a/libfoo.so ./cmd/libfoo
android-x86:
CGO_ENABLED=1 \
GOOS=android \
GOARCH=386 \
CC=$(NDK_BIN)/i686-linux-android21-clang \
go build -buildmode=c-shared -o $(ANDROID_OUT)/x86/libfoo.so ./cmd/libfoo
android-x86_64:
CGO_ENABLED=1 \
GOOS=android \
GOARCH=amd64 \
CC=$(NDK_BIN)/x86_64-linux-android21-clang \
go build -buildmode=c-shared -o $(ANDROID_OUT)/x86_64/libfoo.so ./cmd/libfoo
android: android-armv7a android-arm64 android-x86 android-x86_64
- Go to
android_app/app/src/main/cpp
and do the following: 8.1. FileCMakeLists.txt
, make it as:
cmake_minimum_required(VERSION 3.10.2)
project("android")
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.cpp )
add_library(lib_foo SHARED IMPORTED)
set_property(TARGET lib_foo PROPERTY IMPORTED_NO_SONAME 1)
set_target_properties(lib_foo PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/libfoo.so)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}/)
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
target_link_libraries( # Specifies the target library.
native-lib
lib_foo
# Links the target library to the log library
# included in the NDK.
${log-lib} )
8.2. File native-lib.cpp
make it as:
#include <jni.h>
#include <string>
#include "libfoo.h" // our library header
extern "C" {
void
Java_tk_android_MainActivity_serverJNI() {
// Running the server
server();
}
}
- Add webview to the
layout/activity_main
, as:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:id="@+id/wv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:isScrollContainer="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- Update the
MainActivity
as below:
package tk.android
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var wv = findViewById<WebView>(R.id.web_view)
serverJNI()
wv.loadUrl("http://127.0.0.1:6060/")
wv.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(viewx: WebView, urlx: String): Boolean {
viewx.loadUrl(urlx)
return false
}
}
}
private external fun serverJNI(): Void
companion object {
// Used to load the 'native-lib' library on application startup.
init {
System.loadLibrary("native-lib")
}
}
}
- Update
AndroidManifest
to be:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="tk.android">
<!-- Mandatory:
android:usesCleartextTraffic="true"
Optional:
android:hardwareAccelerated="true"
Depending on the action bar required:
android:theme="@style/Theme.AppCompat.NoActionBar"
-->
<application
android:hardwareAccelerated="true" // <- Optional
android:usesCleartextTraffic="true" // <- A must to be added
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.NoActionBar"> // <- If do not want action bar
<activity android:name=".MainActivity"
android:configChanges="orientation|screenSize"> // <- A Must to avoid crash at rotating
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Bonus
With Go embed
all static files can be embed in the same library, including css
, javascript
, templates
so you can buid either API, or full app with GUI
I uploaded the main file here if any one interested about the topic.
Credit goes to Roger Chapman