Mini App SDK for Android
Provides functionality to show a Mini App in Android Applications. The SDK offers features like downloading, caching, updating, and displaying of a Mini App. Mini App SDK also facilitates communication between a mini app and the host app via a message bridge.
Requirements
Supported Android Versions
This SDK supports Android API level 23 and above.
Getting Started
#1 Add dependency to your app’s build.gradle
repositories {
jcenter()
// The following repo is needed only if you want to use snapshot releases
maven { url 'http://oss.jfrog.org/artifactory/simple/libs-snapshot/' }
}
dependency {
implementation 'com.rakuten.tech.mobile.miniapp:miniapp:${version}'
}
#2 Configure SDK settings in AndroidManifest.xml
The SDK is configured via manifest meta-data, the configurable values are:
Field | Datatype | Manifest Key | Optional | Default |
---|---|---|---|---|
Base URL | String | com.rakuten.tech.mobile.miniapp.BaseUrl |
❌ | 🚫 |
Is Preview Mode | Boolean | com.rakuten.tech.mobile.miniapp.IsPreviewMode |
❌ | 🚫 |
Project ID | String | com.rakuten.tech.mobile.ras.ProjectId |
❌ | 🚫 |
RAS Project Subscription Key | String | com.rakuten.tech.mobile.ras.ProjectSubscriptionKey |
❌ | 🚫 |
Host App User Agent Info | String | com.rakuten.tech.mobile.miniapp.HostAppUserAgentInfo |
✅ | 🚫 |
Note:
- We don’t currently host a public API, so you will need to provide your own Base URL for API requests.
- The host app info is the string which is appended to user-agent of webview. It should be a meaningful keyword such as host app name to differentiate other host apps.
In your AndroidManifest.xml
:
<manifest>
<application>
<!-- Base URL used for retrieving a Mini App -->
<meta-data
android:name="com.rakuten.tech.mobile.miniapp.BaseUrl"
android:value="https://www.example.com" />
<!-- Preview mode used for retrieving the Mini Apps -->
<meta-data
android:name="com.rakuten.tech.mobile.miniapp.IsPreviewMode"
android:value="${isPreviewMode}" />
<!-- Project ID for the Platform API -->
<meta-data
android:name="com.rakuten.tech.mobile.ras.ProjectId"
android:value="your_project_id" />
<!-- Subscription Key for the Platform API -->
<meta-data
android:name="com.rakuten.tech.mobile.ras.ProjectSubscriptionKey"
android:value="your_subscription_key" />
<!-- Optional User Agent Information relating to the host app -->
<meta-data
android:name="com.rakuten.tech.mobile.miniapp.HostAppUserAgentInfo"
android:value="app_name/version_info" />
</application>
</manifest>
#3 Fetch Mini App Info
Information about Mini Apps can be fetched in two different ways: by using MiniApp.listMiniApp
to get a list of info for all Mini Apps, or by using MiniApp.fetchInfo
to get info for a single Mini App. Either method will return MiniAppInfo
objects with info about the Mini App such as name, icon URL, ID, version, etc.
Use MiniApp.listMiniApp
if you want a list of all Mini Apps:
CoroutineScope(Dispatchers.IO).launch {
try {
val miniAppList = MiniApp.instance().listMiniApp()
} catch(e: MiniAppSdkException) {
Log.e("MiniApp", "There was an error retrieving the list", e)
}
}
Or use MiniApp.fetchInfo
if you want info for a single Mini App and already know the Mini App’s ID:
CoroutineScope(Dispatchers.IO).launch {
try {
val miniAppInfo = MiniApp.instance().fetchInfo("MINI_APP_ID")
} catch(e: MiniAppSdkException) {
Log.e("MiniApp", "There was an error retrieving the Mini App info", e)
}
}
Note: This SDK uses suspend
functions, so you should use Kotlin Coroutines when calling the functions. These examples use Dispatchers.IO
, but you can use whichever CoroutineContext
and CouroutineScope
that is appropriate for your App. However, you MUST NOT use Dispatchers.Main
because network requests cannot be performed on the main thread.
#4 Implement the MiniAppMessageBridge
The MiniAppMessageBridge
is used for passing messages between the Mini App (JavaScript) and the Host App (your native Android App) and vice versa. Your App must provide the implementation for these functions and pass this implementation to the MiniApp#create
function.
There are some methods have default implementation but host app can override them to fully control.
Method | Default |
---|---|
getUniqueId | 🚫 |
requestPermission | 🚫 |
requestCustomPermissions | ✅ |
shareContent | ✅ |
The UserInfoBridgeDispatcher
:
Method | Default |
---|---|
getUserName | 🚫 |
getProfilePhoto | 🚫 |
getAccessToken | 🚫 |
val miniAppMessageBridge = object: MiniAppMessageBridge() {
override fun getUniqueId() {
var id: String = ""
// Implementation details to generate a Unique ID
// .. .. ..
return id
}
override fun requestPermission(
miniAppPermissionType: MiniAppPermissionType,
callback: (isGranted: Boolean) -> Unit
) {
// Implementation details to request device permission for location
// .. .. ..
callback.invoke(true)
}
// Implement requestCustomPermissions if HostApp wants to show their own UI for managing permissions
override fun requestCustomPermissions(
permissionsWithDescription: List<Pair<MiniAppCustomPermissionType, String>>,
callback: (List<Pair<MiniAppCustomPermissionType, MiniAppCustomPermissionResult>>) -> Unit
) {
// Implementation details to request custom permissions
// .. .. ..
// pass a list of Pair of MiniAppCustomPermissionType and MiniAppCustomPermissionResult in callback
callback.invoke(list)
}
override fun shareContent(
content: String,
callback: (isSuccess: Boolean, message: String?) -> Unit
) {
// Share content implementation.
// .. .. ..
callback.invoke(true, null) // or callback.invoke(false, "error message")
}
}
val userInfoBridgeDispatcher = object : UserInfoBridgeDispatcher() {
override fun getUserName(): String {
var name: String = ""
// Implementation details to get user name
// .. .. ..
return name
}
override fun getProfilePhoto(): String {
var profilePhotoUrl: String = ""
// Implementation details to get profile photo url
// .. .. ..
return profilePhotoUrl
}
override fun getAccessToken(
miniAppId: String,
onSuccess: (tokenData: TokenData) -> Unit,
onError: (message: String) -> Unit
){
var allowToken: Boolean = false
// Check if you want to allow this Mini App ID to use the Access Token
// .. .. ..
if (allowToken)
onSuccess(tokenData) // allow miniapp to get token and return TokenData value.
else
onError(message) // reject miniapp to get token and with message explanation.
}
}
// set UserInfoBridgeDispatcher object to miniAppMessageBridge
miniAppMessageBridge.setUserInfoBridgeDispatcher(userInfoBridgeDispatcher)
#5 Create and display a Mini App
Calling MiniApp.create
with a Mini App ID object will download the latest version of the Mini App if it has not yet been downloaded. A view will then be returned which will display the Mini App.
class MiniAppActivity : Activity(), CoroutineScope {
override val coroutineContext = Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super(savedInstanceState)
setContentView(R.layout.loading)
val context = this
launch {
try {
val miniAppDisplay = withContext(Dispatchers.IO) {
MiniApp.instance().create("MINI_APP_ID", miniAppMessageBridge)
}
val miniAppView = miniAppDisplay.getMiniAppView(this@MiniAppActivity)
setContentView(miniAppView)
} catch (e: MiniAppSdkException) {
setContentView(R.layout.error_screen)
}
}
}
}
MiniAppDisplay.navigateBackward
and MiniAppDisplay.navigateForward
facilitates the navigation inside a mini app if the history stack is available in it. A common usage pattern could be to link it up to the Android Back Key navigation.
Note: Clearing up the mini app display is essential. MiniAppDisplay.destroyView
is required to be called when exit miniapp.
Note: There are several different types of exceptions which could be thrown by MiniApp.create
, but all are sub-classes of MiniAppSdkException
.
You can handle each exception type differently if you would like different behavior for different cases.
For example you may wish to display a different error message when the server contains no published versions of a mini app.
See the full list of exceptions in the API docs.
Note: Preview Mode
In preview mode, you can have multiple versions of single miniapp so you can load the specific version with MiniAppInfo object by using MiniApp.instance().create(MINI_APP_INFO, miniAppMessageBridge)
.
Advanced
#1 Clearing up mini app display
For a mini app, it is required to destroy necessary view state and any services registered with.
The automatic way can be used only if we want to end the Activity
container along with mini app display. MiniAppDisplay
complies to Android’s LifecycleObserver
contract. It is quite easy to setup for automatic clean up of resources.
class MiniAppActivity : Activity(), CoroutineScope {
override fun onCreate(savedInstanceState: Bundle?) {
//...
launch {
val miniAppDisplay = withContext(Dispatchers.IO) {
MiniApp.instance().create("mini_app_id", miniAppMessageBridge)
}
lifeCycle.addObserver(miniAppDisplay)
//...
}
}
}
To read more about Lifecycle
please see link.
On the other hand, when the consuming app manages resources manually or where it has more control on the lifecycle of views MiniAppDisplay.destroyView
should be called upon e.g. when removing a view from the view system, yet within the same state of parent’s lifecycle.
#2 Navigating inside a mini app
For a common usage pattern, the navigation inside a mini app can be attached to the Android back key navigation as shown:
override fun onBackPressed() {
if(!miniAppDisplay.navigateBackward()) {
super.onBackPressed()
}
}
#3 External url loader
The mini app is loaded with the specific custom scheme and custom domain in mini app view.
In default, the external link will be opened in custom tab. See this.
HostApp also can implement their own way by passing MiniAppNavigator
object to MiniApp.create(appId: String, miniAppMessageBridge: MiniAppMessageBridge, miniAppNavigator: MiniAppNavigator)
.
- Implement
MiniAppNavigator
.
miniAppNavigator = object : MiniAppNavigator {
override fun openExternalUrl(url: String, externalResultHandler: ExternalResultHandler) {
// Load external url with own webview.
}
}
-
Create mini app display Using
MiniApp.instance().create("MINI_APP_ID", miniAppMessageBridge, miniAppNavigator)
. In preview mode, usingMiniApp.instance().create(MINI_APP_INFO, miniAppMessageBridge, miniAppNavigator)
. -
Return URL result to mini app view. Some mini apps are loaded their services with external url but in the end that external url will trigger callback or webhook to redirect to mini app custom scheme, mini app custom domain. The external webview / browser cannot recognize mini app url so it is required the return of that url to mini app view.
There are two approaches to return mini app url from host app webview to mini app view:
Automatic check in WebView which belongs to separated Activity
If the external webview Activity is different from the Activity running mini app, our SDK provide the auto check and Activity closing by overriding the WebViewClient.
val miniAppExternalUrlLoader = MiniAppExternalUrlLoader(miniAppId, externalWebViewActivity)
class MyWebViewClient(private val miniAppExternalUrlLoader: MiniAppExternalUrlLoader): WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url.toString()
return miniAppExternalUrlLoader.shouldOverrideUrlLoading(url)
}
}
Return the url result to mini app view:
// externalResultHandler is from MiniAppNavigator implementation.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == externalWebViewReqCode && resultCode == Activity.RESULT_OK) {
data?.let { intent -> externalResultHandler.emitResult(intent) }
}
}
Manual check by host app
Host app can take full control and transmit the url back to mini app view.
val miniAppExternalUrlLoader = MiniAppExternalUrlLoader(miniAppId, null)
Using miniAppExternalUrlLoader.shouldClose(url)
which returns Boolean
to check if it is
mini app scheme and should close external webview.
Using #ExternalResultHandler.emitResult(String)
to transmit the url string to mini app view.
#4 Custom Permissions
MiniApp Android SDK supports list of Custom Permissions (MiniAppCustomPermissionType
) and these can be stored and retrieved using the following public interfaces.
Retrieving the Mini App Custom Permissions using MiniAppID
Custom permissions and its status can be retrieved using the following interface. getCustomPermissions
will return MiniAppCustomPermission
that contains the meta-info as a Pair
of
name and grant result (ALLOWED
or DENIED
). The custom permissions are stored per each miniAppId.
val permissions = miniapp.getCustomPermissions(miniAppId)
Store the Mini App Custom Permissions
Custom permissions for a mini app are cached by the SDK and you can use the following interface to store permissions when needed.
var permissionPairs = listOf<Pair<MiniAppCustomPermissionType, MiniAppCustomPermissionResult>>()
// .. .. ..
val permissionsToSet = MiniAppCustomPermission(miniAppId, permissionPairs)
miniapp.setCustomPermissions(permissionsToSet)
List of downloaded Mini apps with Custom Permissions
val downloadedMiniApps = miniapp.listDownloadedWithCustomPermissions()
downloadedMiniApps.forEach {
val miniApp = it.first
val permissions = it.second
// Display permissions in view, etc....
}
#5 Ads Integration
It is optional to set AdMob for mini apps to show advertisement. The below implementation will allow ads to be shown when mini apps trigger a request.
Configure the Android Ads SDK from here. Don’t forget to initialize the Ads SDK.
AdMob
Set the AdMobDisplayer
provided by MiniApp SDK. This controller will handle the display of ad so no work is required from host app.
miniAppMessageBridge.setAdMobDisplayer(AdMobDisplayer(activityContext))
Custom Ads Provider
In case the host app wants to take control of the ad display, there is the interface MiniAppAdDisplayer
to implement.
class CustomAdDisplayer: MiniAppAdDisplayer {
override fun loadInterstitialAd(adUnitId: String, onLoaded: () -> Unit, onFailed: (String) -> Unit) {
// load the ad
}
override fun showInterstitialAd(adUnitId: String, onClosed: () -> Unit, onFailed: (String) -> Unit) {
// show the ad
}
//...more ad implementations.
}
miniAppMessageBridge.setAdMobDisplayer(CustomAdDisplayer())
#6 Screen Orientation
The default setting does not allow miniapp to change hostapp screen orientation. Hostapp can allow miniapp to control the screen orientation for better experience by calling
miniAppMessageBridge.allowScreenOrientation(true)
In case miniapp is allowed to control, please ensure that your activity handles screen orientation.
There are several ways to prevent the view from being reset.
In our Demo App, we set the config on activity android:configChanges="orientation|screenSize"
.
See here.
Troubleshooting
Exception: “Network requests must not be performed on the main thread.”
Some of the suspending functions in this SDK will perform network requests (MiniApp.create
, MiniApp.fetchInfo
, MiniApp.listMiniApp
). Network requests should not be performed on the main thread, so the above exception will occur if your Coroutine is running in the Dispatchers.Main
CoroutineContext. To avoid this exception, please use the Dispatchers.IO
or Dispatchers.Default
context instead. You can use withContext
to make sure your code is running in the appropriate CoroutineContext.
CoroutineScope(Dispatchers.Main).launch {
withContext(Dispatchers.IO) {
// Call MiniApp suspending function i.e. `MiniApp.create`
// This runs in a background thread
}
// Update your UI - i.e. `setContentView(miniAppView)`
// This runs on the main thread
}
Changelog
See the full CHANGELOG.