This guide is a work in progress as the joynr Android api has not been finalized.
In order to see an example implementation you can check android-hello-world-binder provider and consumer examples for a more concrete example of implementing an Android joynr application using MVVM pattern.
See the Java Configuration Reference for a complete list of all available configuration properties available to use in joynr Java applications, which has very similar classes to Android.
If you are planning on taking advantage of multi-user in the chosen Android environment, check out the Multi-User in Android section.
To develop an Android project with Android Studio first you need to create a new project following the guidelines in "Create an Android Project" .
In order to use joynr java code generator you need to add the generator gradle plugin in the project build.gradle, as it needs to be applied in the app.
dependencies {
...
classpath "io.joynr.tools.generator:joynr-generator-gradle-plugin:$JOYNR_VERSION"
...
}
Note: The version tag needs to be replaced with the version you want to use, check here the available versions
Then in the application build.gradle the joynr generator gradle plugin needs to be applied in order to generate the joynr code through the fidl file. This can be done by adding the following line in the beginning of your app/build.gradle
apply plugin: 'io.joynr.tools.generator.joynr-generator-gradle-plugin'
And now in the dependencies you can add the joynr-android-binder-runtime dependency in order use it in your program. For this in app/build.gradle you should add the following lines:
dependencies {
...
implementation "io.joynr.android:joynr-android-binder-runtime:$JOYNR_VERSION"
...
}
Note: The version tag needs to be replaced with the version you want to use, check here the available versions
To finalize the environment create a fidl folder app/src/main/ and add a file with the following contents
package helloworld
typeCollection {
const String hello = "Hello World!"
}
interface HelloWorld {
attribute String hello
}
Then when you sync Gradle or build the project, new files will be generated in app/build/generated/source/fidl/joynr/ from the interfaces defined in the fidl files.
With everything setup we will start to create a main application and also a consumer and provider class
In an Android project it is recommended to use a class that extends from the Application class and uses that class as a singleton to manage the joynr runtime, just like in the following example.
class HelloWorldApplication: Application() {
// Must be included for logs to work
private val logger = LoggerFactory.getLogger(HelloWorldApplication::class.java)
// The object that will contain the runtime
private lateinit var runtime: JoynrRuntime
override fun onCreate() {
super.onCreate()
// This is where you can define the debug level
StaticLoggerBinder.setLogLevel(AndroidLogger.LogLevel.DEBUG)
// Initializing Android Binder Runtime with Application Context
// This needs to be in the Application onCreate so the JoynrRuntime is
// ready to be used immediatly after the application is started
runtime = AndroidBinderRuntime.init(this)
...
}
}
Then add your application to the Android Manifest file
<manifest
...
>
<application
android:name=".HelloWorldApplication"
...
>
</application>
...
</manifest>
Using this approach you can create the runtime on the program initialization and access it anywhere in your application.
The provider class in this example is just a provider manager and a bridge between the provider and the ViewModel, all the logic of the provider is stored in the provider class.
The logic behind the initialization of this class is very similar to the one behind the Consumer
We create a class based on the HelloWorldAbstractProvider generated by the fidl file with a simple hello message
/**
This is where you define your Provider
*/
class HelloWorldProvider: HelloWorldAbstractProvider() {
var hello: String = "Hello"
/**
These will be the functions available to the Consumers to call on this Provider
*/
// This returs the hello String to the Consumer
override fun getHello(): Promise<Deferred<String>> {
val deferred = Deferred<String>()
deferred.resolve(hello)
return Promise(deferred)
}
// This sets the String to be returned in future uses
override fun setHello(hello: String?): Promise<DeferredVoid> {
val deferredVoid = DeferredVoid()
this.hello = hello!!
deferredVoid.resolve()
return Promise(deferredVoid)
}
}
Then we will need to register the provider we created, in the main application
// Register Provider with the Cluster Controller
// "domain" is the identifier that will identify your provider in the Cluster Controller
runtime.getProviderRegistrar("domain", HelloWorldProvider())
.register()
You should make this call in the onCreate (after the init method) method of the application
And after that the Model just waits for the the data in the provider to be updated and passes that information to the ViewModel.
The consumer view model will have a proxy that links the data with the provider and a data attribute
class ConsumerViewModel : ViewModel() {
private lateinit var helloWorldProxy: HelloWorldProxy
private val _providedStr = MutableLiveData<String>()
val providedStr: LiveData<String> by lazy {
_providedStr
}
...
}
After setting up the Application class we need to initialize the consumer proxy
...
fun registerProxy(application: Application) {
val discoveryQos = DiscoveryQos()
discoveryQos.discoveryTimeoutMs = 10000
discoveryQos.discoveryScope = DiscoveryScope.LOCAL_ONLY
discoveryQos.cacheMaxAgeMs = java.lang.Long.MAX_VALUE
discoveryQos.arbitrationStrategy = ArbitrationStrategy.HighestPriority
val app = application as HelloWorldApplication
val runtime = app.getJoynrRuntime()
helloWorldProxy = runtime.getProxyBuilder("domain", HelloWorldProxy::class.java)
.setMessagingQos(MessagingQos()).setDiscoveryQos(discoveryQos).build()
}
...
The proxy requests have a similiar api to Java synchronous and asynchronous procedures.
This data is linked to the ViewModel by using LiveData. This means that when the value of the string is updated, an event is triggered notifying the ViewModel of that change.
After that we need to setup how we are going to get the information coming from the providers and we will be using callbacks to ensure that the main thread will not be blocked
...
fun getString() {
helloWorldProxy.getHello(
object : Callback<String>() {
override fun onSuccess(s1: String?) {
Log.d("Received Message", s1!!)
// Do some logic
_providedStr.postValue(s1)
}
override fun onFailure(e: JoynrRuntimeException) {
Log.d("Failed Message", "Failed to receive message with the error: $e")
_providedStr.postValue(e.message)
}
}
)
}
Note: The callback import must be the one from
io.joynr.proxy.Callback
We then need to register the view model and also setup our listeners and observers
class MainActivity : AppCompatActivity() {
lateinit var consumerViewModel: ConsumerViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
consumerViewModel = ViewModelProvider(this).get(ConsumerViewModel::class.java)
consumerViewModel.registerProxy(application)
talk_button.setOnClickListener {
consumerViewModel.getString()
}
consumerViewModel.providedStr.observe(this, Observer { str ->
text_box.text = str
})
}
}
In the Android world, there is the concept of multi-user where different users may have installations of the same components and applications. Developers that want to consider multi-user during development should be aware of how the joynr Android Binder runtime works.
In joynr, it is understood that the Cluster Controller (CC) runs in user 0, which is the system user. This CC is like the server part in a client-server architecture. Multiple clients, which are joynr components/applications that implement Proxies and Providers, connect to this server. This means that the CC is actually a singleton, and only exists in user 0.
The Android Binder runtime performs logic on how to bind to the joynr BinderService by using the information provided in the BinderAddress. By default, when binding to the Service, the runtime will bind to the CC's Service as user 0 and will bind to any other client Services as their respective user. This information extraction happens under the hood, as the runtime fetches and uses the user ID automatically when creating the BinderAddress.
In order for all of this to work as it should, the implementation of your CC should include the
permissions android.permission.INTERACT_ACROSS_USERS
and
android.permission.INTERACT_ACROSS_USERS_FULL
. These permissions allow the CC to successfully
bind to the Service in different users. Note that these permissions require the CC to be a system
app, as only those can request them.
You should also make sure that joynr's BinderService is declared as singleUser=true
in the CC's
Android Manifest file. What this does is effectively make the CC's BinderService a singleton within
the platform, which will always run in user 0. This can be done by declaring the Service again in
the Manifest like so:
<!-- merged from joynr Binder runtime dependency but we want to redeclare it -->
<service
android:name="io.joynr.android.binder.BinderService"
android:enabled="true"
android:exported="true"
android:singleUser="true">
<intent-filter>
<action android:name="io.joynr.android.action.COMMUNICATE" />
</intent-filter>
</service>
Developers of any client applications or components that need to make use of multi-user should be aware that they need to understand their use cases and see how to best implement these scenarios. You can make the following components single user in Android: Service, Receiver, or Content Provider. Activities can not be declared as single user, which means they will be recreated for each user that is created in the system and uses the app.
Depending on use case, you can choose a strategy where you declare these components as single user, perform their joynr Provider/Proxy execution, and then share results across all users that want to make use of the data. For example, you can register a joynr Provider in a single-user Android Content Provider, store retrieved data there, and then the application retrieves this information from the Content Provider (which in this scenario is the single source of truth), available as a singleton in user 0.
Always remember that any component within the system that is not declared as single user will be run for every user in the system! This means that you need extra care to ensure that neither the CC nor joynr apps, unless required, have any such components, and if they do, you must be aware of this functioning, otherwise things might not work as intended.
You can read more about Android multi-user development in Android's official documentation.