Hi,
I went through the Android developers tutorial for RenderScript (allows to compute on GPU using C99 and java). I took the code out of the samples and I have a problem with a non recognized function:
In HelloCompute.java main class there is this line:
Code:
mScript.forEach_root()
Eclipse can't find the forEach_root() function.
I looked into the generated java class of the C part, which should be called by the forEach_root() java call:
Code:
package com.example.android.rs.hellocompute;
import android.renderscript.*;
import android.content.res.Resources;
/**
* @hide
*/
public class ScriptC_mono extends ScriptC {
private static final String __rs_resource_name = "mono";
// Constructor
public ScriptC_mono(RenderScript rs) {
this(rs,
rs.getApplicationContext().getResources(),
rs.getApplicationContext().getResources().getIdentifier(
__rs_resource_name, "raw",
rs.getApplicationContext().getPackageName()));
}
public ScriptC_mono(RenderScript rs, Resources resources, int id) {
super(rs, resources, id);
__U8_4 = Element.U8_4(rs);
}
private Element __U8_4;
}
An Idea anyone?
Thanks,
DP
darkPrint said:
Hi,
I went through the Android developers tutorial for RenderScript (allows to compute on GPU using C99 and java). I took the code out of the samples and I have a problem with a non recognized function:
In HelloCompute.java main class there is this line:
Code:
mScript.forEach_root()
Eclipse can't find the forEach_root() function.
I looked into the generated java class of the C part, which should be called by the forEach_root() java call:
Code:
package com.example.android.rs.hellocompute;
import android.renderscript.*;
import android.content.res.Resources;
/**
* @hide
*/
public class ScriptC_mono extends ScriptC {
private static final String __rs_resource_name = "mono";
// Constructor
public ScriptC_mono(RenderScript rs) {
this(rs,
rs.getApplicationContext().getResources(),
rs.getApplicationContext().getResources().getIdentifier(
__rs_resource_name, "raw",
rs.getApplicationContext().getPackageName()));
}
public ScriptC_mono(RenderScript rs, Resources resources, int id) {
super(rs, resources, id);
__U8_4 = Element.U8_4(rs);
}
private Element __U8_4;
}
An Idea anyone?
Thanks,
DP
Click to expand...
Click to collapse
Okay, it was just a corrupted project issue. Loosing so much time for such a stupid issue...
Well, know you know, if you get to this error, clean the project and rebuid, that's it.
Cheers,
DP
Related
Hi everyone
I'm fairly sure this is the right place to put this, but please correct me if I'm wrong.
I'm trying to create an app that take GPS coordinates from the inbuilt system and output it via an IOIO board (just a board that allows the phone to have digital and analogue output) so that it can be transmitted via a separate module to a receiver. The system will then be used to track a balloon.
I made some headway, I can send "Hello World" easily, but simply can't make the LocationManager work for me as using that just stops everything from working. My Java skills leave a lot to be desired, so I was hoping someone could check over my code and look for a fundamental mistake. The problem appears LocationUpdateHandler class, as relying on that to generate the 'stuff' variable simply halts the entire program.
It is based on two pieces of sample code that came with the IOIO board, but the GPS part is my own.
Code:
package ioio.examples.hello;
import ioio.lib.api.DigitalOutput;
import ioio.lib.api.exception.ConnectionLostException;
import ioio.lib.util.BaseIOIOLooper;
import ioio.lib.util.IOIOLooper;
import ioio.lib.util.android.IOIOActivity;
import android.content.Context;
import android.os.Bundle;
import android.widget.ToggleButton;
import ioio.lib.api.Uart;
import java.io.OutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
/**
* This is the main activity of the HelloIOIO example application.
*
* It displays a toggle button on the screen, which enables control of the
* on-board LED. This example shows a very simple usage of the IOIO, by using
* the {@link IOIOActivity} class. For a more advanced use case, see the
* HelloIOIOPower example.
*/
public class MainActivity extends IOIOActivity {
private ToggleButton button_;
private LocationManager locationManager;
String provider;
String stuff;
/**
* Called when the activity is first created. Here we normally initialize
* our GUI.
*/
[user=439709]@override[/user]
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
button_ = (ToggleButton) findViewById(R.id.button);
//Sets up the location manager and listener. I feel this section is the root of my problems
LocationListener locationListener = new LocationUpdateHandler();
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 35000, 10, new LocationUpdateHandler());
}
//Should, in theory, set the global variable 'stuff' to be the location of the phone stored as a string
public class LocationUpdateHandler implements LocationListener {
public void onLocationChanged(Location loc) {
double lat = loc.getLatitude();
double lng = loc.getLongitude();
stuff = Double.toString(lat) + "$" + Double.toString(lng) + "&";
}
[user=439709]@override[/user]
public void onProviderDisabled(String provider) {}
[user=439709]@override[/user]
public void onProviderEnabled(String provider) {}
[user=439709]@override[/user]
public void onStatusChanged(String provider, int status,
Bundle extras) {}
}
/**
* This is the thread on which all the IOIO activity happens. It will be run
* every time the application is resumed and aborted when it is paused. The
* method setup() will be called right after a connection with the IOIO has
* been established (which might happen several times!). Then, loop() will
* be called repetitively until the IOIO gets disconnected.
*/
class Looper extends BaseIOIOLooper {
/** The on-board LED. */
private DigitalOutput led_;
Uart uart;
OutputStream outGPS;
/**
* Called every time a connection with IOIO has been established.
* Typically used to open pins.
*
* [user=948141]@Throw[/user]s ConnectionLostException
* When IOIO connection is lost.
*
* [user=690402]@see[/user] ioio.lib.util.AbstractIOIOActivity.IOIOThread#setup()
*/
[user=439709]@override[/user]
public void setup() throws ConnectionLostException {
led_ = ioio_.openDigitalOutput(0, true);
uart = ioio_.openUart(3,4, 9600, Uart.Parity.NONE, Uart.StopBits.ONE);
outGPS = uart.getOutputStream();
}
public void loop() throws ConnectionLostException {
led_.write(!button_.isChecked());
byte[] GPSCoordinate = null;
//If I set stuff to "Hello World" here the IOIO board succesfully transmits hello world
try {
GPSCoordinate = stuff.getBytes("US-ASCII");
} catch (UnsupportedEncodingException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
outGPS.write(GPSCoordinate);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
[user=439709]@override[/user]
protected IOIOLooper createIOIOLooper() {
return new Looper();
}
}
Thank you for your help,
Spanners.
and sorry about the code dump, but I think pretty much all of the code is important to understand the program.
Thanks.
I hope this is the right place to ask this. The other sections seem to be more about discussion. I am running into one error that I have not encountered before and have tried quite a bit to find out what is going on.
AndroidStudio said:
Error: (109, 51) error: non-static method getAllContacts() cannot be referenced from a static context
Click to expand...
Click to collapse
Code:
// Class for dealing with the database
package com.testapplication.app;
import ...
public class DataBaseHandler extends SQLiteOpenHelper {
public List<Contact> getAllContacts() {
List<Contact> contListAdapt = new ArrayList<Contact>();
// Becomes non static in a static context if the method becomes static
SQLiteDatabase sql = getWritableDatabase();
Cursor cursor = sql.rawQuery("SELECT * FROM " + TABLE_CONTACTS, null);
if (cursor.moveToFirst()) {
do {
new Contact(Integer.parseInt(cursor.getString(0)), cursor.getString(1), cursor.getString(2), cursor.getString(3), cursor.getString(4), Uri.parse(cursor.getString(5)));
} while (cursor.moveToNext());
}
cursor.close();
sql.close();
return contListAdapt;
}
}
Code:
// The main activity
package com.testapplication.app;
import ...
public class MainActivity extends ActionBarActivity {
List<Contact> contactListAdapter = new ArrayList<Contact>();
@Override
protected void onCreate (Bundle savedInstanceState) {
// There is a class called Contact
// FIXME : non static method in static context
Collection<Contact> coll = DataBaseHandler.getAllContacts();
if(!coll.isEmpty()) {
contactListAdapter.addAll(coll);
}
}
}
As far as I can tell this is all that is involved with this error. If I make getAllContacts() static then I will get the same old error but in a different spot: getWritableDatabase();
Ok, so I'm a very newbe programmer. I do know a little C++. But I wanted to see if I could create a basic Hello World android app. I found a video on YouTube that I'm trying to follow along. I'm pretty sure my code is identical to his code. But mine won't compile.
Here's the code...
package com.example.simplehelloworldapplication;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private TextView textView;
private Button button;
@override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView=findViewById(R.id.text_view);
button=findViewById(R.id.Button_click);
button.setOnClickListener(new View.OnClickListener() {
boolean visible;
@override
public void onClick(View v) {
visible = !visible;
button.setVisibility(visible? View.VISIBLE:View.VISIBLE);
}
});
}
}
When I attempt to compile the code, I get 3 errors...
error: incompatible types: no unique maximal instance exists for type variable T with upper bounds Button,View
(It's refering to this error: button=findViewById(R.id.Button_click)
error: cannot find symbol method setVisibility(int)
error: cannot find symbol method setOnClickListener(<anonymous OnClickListener>)
Any suggestions to fix these errors would be greatly appreciated.
Ok, so I figured it out. Not from the code above, but I found training on androids website that showed me how to do it and it worked. I now have an app on my phone I created that asks for input and then sends it (to nowhere)
Introduction
React Native is a convenient tool for cross platform development, and though it has become more and more powerful through the updates, there are limits to it, for example its capability to interact with and using the native components. Bridging native code with Javascript is one of the most popular and effective ways to solve the problem. Best of both worlds!
Currently not all HMS Kits has official RN support yet, this article will walk you through how to create android native bridge to connect your RN app with HMS Kits, and Scan Kit will be used as an example here.
The tutorial is based on https://github.com/clementf-hw/rn_integration_demo/tree/4b2262aa2110041f80cb41ebd7caa1590a48528a, you can find more details about the sample project in this article: https://forums.developer.huawei.com...d=0201230857831870061&fid=0101187876626530001.
Prerequisites
Basic Android development
Basic React Native development
These areas have been covered immensely already on RN’s official site, this forum and other sources
HMS properly configured
You can also reference the above article for this matter
Major dependencies
RN Version: 0.62.2 (released on 9th April, 2020)
Gradle Version: 5.6.4
Gradle Plugin Version: 3.6.1
agcp: 1.2.1.301
This tutorial is broken into 3 parts:
Pt. 1: Create a simple native UI component as intro and warm up
Pt. 2: Bridging HMS Scan Kit into React Native
Pt. 3: Make Scan Kit into a stand alone React Native Module that you can import into other projects or even upload to npm.
Bridging HMS Scan Kit
Now we have some fundamental knowledge on how to bridge, let’s bridge something meaningful. We will bridge the Scan Kit Default View as a QR Code Scanner, and also learn how to communicate from Native side to React Native side.
First, we’ll have to configure the project following the guide to set Scan Kit up on the native side: https://developer.huawei.com/consumer/en/doc/development/HMS-Guides/scan-preparation-4
Put agconnect-service.json in place
Add to allprojects > repositories in root level build.gradle
Code:
allprojects {
repositories {
google()
jcenter()
maven {url 'https://developer.huawei.com/repo/'}
}
}
Add to buildscript > repositories
Code:
buildscript {
repositories {
google()
jcenter()
maven {url 'https://developer.huawei.com/repo/'}
}
}
Add to buildscript > dependencies
Code:
buildscript{
dependencies {
classpath 'com.huawei.agconnect:agcp:1.2.1.301'
}
}
Go to app/build.gradle and add this to header
Code:
apply plugin: 'com.huawei.agconnect'
Add this to dependencies
Code:
dependencies {
implementation 'com.huawei.hms:scanplus:1.1.3.300'
}
Add in proguard-rules.pro
Code:
-ignorewarnings
-keepattributes *Annotation*
-keepattributes Exceptions
-keepattributes InnerClasses
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
-keep class com.hianalytics.android.**{*;}
-keep class com.huawei.**{*;}
Now do a gradle sync. Also you can try to build and run the app to see if everything’s ok even though we have not done any actual development yet.
Add these to AndroidManifest.xml
Code:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<application
…
<activity android:name="com.huawei.hms.hmsscankit.ScanKitActivity" />
</application>
So the basic setup/configuration is done. Similar to the warm up, we will create a Module file first. Note that for the sake of variance and wider adaptability of the end product, this time we’ll make it a plain Native Module instead of Native UI Component.
Code:
package com.cfdemo.d001rn;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
public class ReactNativeHmsScanModule extends ReactContextBaseJavaModule {
private static final String REACT_CLASS = "ReactNativeHmsScan";
private static ReactApplicationContext reactContext;
public ReactNativeHmsScanModule(ReactApplicationContext context) {
super(context);
reactContext = context;
}
@NonNull
@Override
public String getName() {
return REACT_CLASS;
}
}
We have seen how data flows from RN to native in the warm up (e.g. @reactProp of our button), There are also several ways for data to flow from native to RN. In Scan Kit, it utilizes startActivityForResult, therefore we need to implement its subsequent listeners.
Code:
package com.cfdemo.d001rn;
import android.app.Activity;
import android.content.Intent;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.BaseActivityEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
public class ReactNativeHmsScanModule extends ReactContextBaseJavaModule {
private static final String REACT_CLASS = "ReactNativeHmsScan";
private static ReactApplicationContext reactContext;
public ReactNativeHmsScanModule(ReactApplicationContext context) {
super(context);
reactContext = context;
reactContext.addActivityEventListener(mActivityEventListener);
}
@NonNull
@Override
public String getName() {
return REACT_CLASS;
}
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
}
};
}
There are couple small details we’ll need to add. First, React Native javascript side expects a Promise from the result.
Code:
private Promise mScannerPromise;
We also need to add a request code to identify that this is our scan kit activity. 567 here is just an example, the value is of your own discretion
Code:
private static final int REQUEST_CODE_SCAN = 567
There will be several error/reject conditions, let’s identify and declare their code first
Code:
private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
private static final String E_SCANNER_CANCELLED = "E_SCANNER_CANCELLED";
private static final String E_FAILED_TO_SHOW_SCANNER = "E_FAILED_TO_SHOW_SCANNER";
private static final String E_INVALID_CODE = "E_INVALID_CODE";
At this moment, the module should look like this
Code:
package com.cfdemo.d001rn;
import android.app.Activity;
import android.content.Intent;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.BaseActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
public class ReactNativeHmsScanModule extends ReactContextBaseJavaModule {
private static final String REACT_CLASS = "ReactNativeHmsScan";
private static ReactApplicationContext reactContext;
private Promise mScannerPromise;
private static final int REQUEST_CODE_SCAN = 567;
private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
private static final String E_SCANNER_CANCELLED = "E_SCANNER_CANCELLED";
private static final String E_FAILED_TO_SHOW_SCANNER = "E_FAILED_TO_SHOW_SCANNER";
private static final String E_INVALID_CODE = "E_INVALID_CODE";
public ReactNativeHmsScanModule(ReactApplicationContext context) {
super(context);
reactContext = context;
reactContext.addActivityEventListener(mActivityEventListener);
}
@NonNull
@Override
public String getName() {
return REACT_CLASS;
}
private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
}
};
}
Now let’s implement the listener method
Code:
if (requestCode == REQUEST_CODE_SCAN) {
if (mScannerPromise != null) {
if (resultCode == Activity.RESULT_CANCELED) {
mScannerPromise.reject(E_SCANNER_CANCELLED, "Scanner was cancelled");
} else if (resultCode == Activity.RESULT_OK) {
Object obj = intent.getParcelableExtra(ScanUtil.RESULT);
if (obj instanceof HmsScan) {
if (!TextUtils.isEmpty(((HmsScan) obj).getOriginalValue())) {
mScannerPromise.resolve(((HmsScan) obj).getOriginalValue().toString());
} else {
mScannerPromise.reject(E_INVALID_CODE, "Invalid Code");
}
return;
}
}
}
}
Let’s walk through what this does
When the listener receives an activity result, it checks if this is our request by checking the request code.
Afterwards, it checks if the promise object is null. We will cover the promise object later, but briefly speaking this is passed from RN to native, and we rely on it to send the data back to RN.
Then, if the result is a CANCELED situation, we tell RN that the scanner is canceled, for example closed by user, by calling promise.reject()
If the result indicates OK, we’ll get the data by calling getParcelableExtra()
Now we’ll see if the resulting data matches our data type and is not empty, and then we’ll call promise.resolve()
Otherwise we will resolve a general rejection message. Of course here you can expand and give a more detailed breakdown and resolution if you wish
This is a lot of checking and validation, but one can never be too safe, right?
Cool, now we have finished the listener, let’s work on the caller! This is the method we’ll be calling in RN side, indicated by the @reactMethod annotation.
[CODE @reactMethod
public void startScan(final Promise promise) {
} [/CODE]
Give it some content
[CODE @reactMethod
public void startScan(final Promise promise) {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist");
return;
}
// Store the promise to resolve/reject when picker returns data
mScannerPromise = promise;
try {
ScanUtil.startScan(currentActivity, REQUEST_CODE_SCAN, new HmsScanAnalyzerOptions.Creator().setHmsScanTypes(HmsScan.ALL_SCAN_TYPE).create());
} catch (Exception e) {
mScannerPromise.reject(E_FAILED_TO_SHOW_SCANNER, e);
mScannerPromise = null;
}
}[/CODE]
Let’s do a walk through again
First we get the current activity reference and check if it is valid
Then we take the input promise and assign it to mScannerPromise which we declared earlier, so we can refer and use it throughout the process
Now we call the Scan Kit! This part is same as a normal android implementation.
Of course we wrap it with a try-catch for safety purposes
At this point we have finished the Module, same as the warm up we’ll need to create a Package. This time it is a Native Module therefore we register it in createNativeModules() and also give createViewManagers() an empty list.
Code:
package com.cfdemo.d001rn;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
public class ReactNativeHmsScanPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new ReactNativeHmsScanModule(reactContext));
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
Same as before, we’ll add the package to our MainApplication.java, import the Package, and add it in the getPackages() function
Code:
import com.cfdemo.d001rn.ReactNativeWarmUpPackage;
import com.cfdemo.d001rn.ReactNativeHmsScanPackage;
public class MainApplication extends Application implements ReactApplication {
...
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new ReactNativeWarmUpPackage());
packages.add(new ReactNativeHmsScanPackage());
return packages;
}
All set! Let’s head back to RN side. This is our app from the warm up exercise(with a bit style change for the things we are going to add)
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
Let’s add a Button and set its onPress property as this.onScan() which we’ll implement after this
Code:
render() {
const { displayText, region } = this.state
return (
<View style={styles.container}>
<Text style={styles.textBox}>
{displayText}
</Text>
<RNWarmUpView
style={styles.nativeModule}
text={"Render in Javascript"}
/>
<Button
style={styles.button}
title={'Scan Button'}
onPress={() => this.onScan()}
/>
<MapView
style={styles.map}
region={region}
showCompass={true}
showsUserLocation={true}
showsMyLocationButton={true}
>
</MapView>
</View>
);
}
Reload and see the button
Similar to the one in the warm up, we can declare the Native Module using this simple way
Code:
const RNWarmUpView = requireNativeComponent('RCTWarmUpView')
const RNHMSScan = NativeModules.ReactNativeHmsScan
Now we’ll implement onScan() which uses the async/await syntax for asynchronous coding
Code:
async onScan() {
try {
const data = await RNHMSScan.startScan();
// handle your data here
} catch (e) {
console.log(e);
}
}
Important! Scan Kit requires CAMERA and READ_EXTERNAL_STORAGE permissions to function, make sure you have handled this beforehand. One of the recommended way to handle it is to use react-native-permissions library https://github.com/react-native-community/react-native-permissions. I will make another article regarding this topic, but for now you can refer to https://github.com/clementf-hw/rn_integration_demo if you are in need.
Now we click…TADA!
In this demo, this is what onScan() contains
Code:
async onScan() {
try {
const data = await RNHMSScan.startScan();
const qrcodeData = {
message: (JSON.parse(data)).message,
location: (JSON.parse(data)).location,
my_location: (JSON.parse(data)).my_location
}
this.handleData(qrcodeData)
} catch (e) {
console.log(e);
}
}
Note: one minor modification is needed if you are basing on the branch of this demo project mentioned before
Code:
onLocationReceived(locationData) {
const location = typeof locationData === "object" ? locationData : JSON.parse(locationData)
…
Now let’s try scan this
The actual data contained in the QR Code is
Code:
{"message": "Auckland", "location": {"lat": "-36.848461","lng": "174.763336"}}
Which bring us to Auckland!
Now your HMS Scan Kit in React Native is up and running!
Pt. 2 of this tutorial is done, please feel free to ask questions. You can also check out the repo of the sample project on github: https://github.com/clementf-hw/rn_integration_demo, and raise issue if you have any question or any update.
In the 3rd and final part of this tutorial, we'll go through how to make this RN HMS Scan Kit Bridge a standalone, downloadable and importable React Native Module, which you can use in multiple projects instead of creating the Native Module one by one, and you can even upload it to NPM to share with other fellow developers.
{
"lightbox_close": "Close",
"lightbox_next": "Next",
"lightbox_previous": "Previous",
"lightbox_error": "The requested content cannot be loaded. Please try again later.",
"lightbox_start_slideshow": "Start slideshow",
"lightbox_stop_slideshow": "Stop slideshow",
"lightbox_full_screen": "Full screen",
"lightbox_thumbnails": "Thumbnails",
"lightbox_download": "Download",
"lightbox_share": "Share",
"lightbox_zoom": "Zoom",
"lightbox_new_window": "New window",
"lightbox_toggle_sidebar": "Toggle sidebar"
}
In this tutorial, we will be discussing and implementing the HarmonyOS MVVM Architectural pattern in our Harmony app.
This project is available on github, link can be found at the end of the article
Table of contents
What is MVVM
Harmony MVVM example project structure
Adding dependencies
Model
Layout
Retrofit interface
ViewModel
Tip and Tricks
Conclusion
Recommended resources
What is MVVM
MVVM stands for Model, View, ViewModel:
Model: This holds the data of the application. It cannot directly talk to the View. Generally, it’s recommended to expose the data to the ViewModel through ActiveDatas (Observables ).
View: It represents the UI of the application devoid of any Application Logic. It observes the ViewModel.
ViewModel: It acts as a link between the Model and the View. It’s responsible for transforming the data from the Model. It provides data streams to the View. It also uses hooks or callbacks to update the View. It’ll ask for the data from the Model.
MVVM can be achieved in two ways:
Using Data binding
RxJava
In this tutorial we will implement MVVM in Harmony using RXjava, as Data binding is still under development and not ready to use in Harmony.
Harmony MVVM example project structure
We will create packages by features. It will make your code more modular and manageable.
Adding the Dependencies
Add the following dependencies in your module level build.gradle file:
Code:
dependencies {
//[...]
//RxJava
implementation "io.reactivex.rxjava2:rxjava:2.2.17"
//retrofit
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation "com.squareup.retrofit2:converter-moshi:2.6.0"
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
//RxJava adapter for retrofit
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1'
}
Model
The Model would hold the user’s email and password. The following User.java class does it:
Code:
package com.megaache.mvvmdemo.model;
public class User {
private String email;
private String password;
public User(String email, String password) {
this.email = email;
this.password = password;
}
public void setEmail(String email) {
this.email = email;
}
public String getEmail() {
return email;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
@Override
public String toString() {
return "User{" +
"email='" + email + '\'' +
", password='" + password + '\'' +
'}';
}
}
Layout
NOTE: For this tutorial, I have decided to create the layout for smart watch devices, however it will work fine on all devices, you just need to re-arrange the components and modify the alignment.
The layout will consist of login button, two text fields and two error texts, each will be shown or hidden depending on the value of the text box above it, after clicking the login button. the final UI will like the screenshot below:
Before we create layout lets add some colors:
First create file color.json under resources/base/element and add the following json content:
Code:
{
"color": [
{
"name": "primary",
"value": "#283148"
},
{
"name": "primaryDark",
"value": "#283148"
},
{
"name": "accent",
"value": "#06EBBF"
},
{
"name": "red",
"value": "#FF406E"
}
]
}
Then, lets design background elements for the Text Fields and the Button:
Create file background_text_field.xml and background_text_button.xml under resources/base/graphic as shown in below screenshot :
Then add the following code:
Background_text_field.xml:
Code:
<?xml version="1.0" encoding="UTF-8" ?>
<shape
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:shape="rectangle">
<corners
ohos:radius="20"/>
<solid
ohos:color="#ffffff"/>
<stroke
ohos:width="2"
ohos:color="$color:accent"/>
</shape>
Background_button.xml:
Code:
<?xml version="1.0" encoding="UTF-8" ?>
<shape
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:shape="rectangle">
<corners
ohos:radius="20"/>
<solid
ohos:color="$color:accent"/>
</shape>
Now lets create the background element for the main layout, let’s called background_ability_login.xml:
Code:
<?xml version="1.0" encoding="UTF-8" ?>
<shape xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:shape="rectangle">
<solid
ohos:color="$color:primaryDark"/>
</shape>
Finally, let’s create the layout file ability_login.xml:
Code:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:id="$+id:scrollview"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="$graphic:background_ability_login"
ohos:layout_alignment="horizontal_center"
ohos:rebound_effect="true"
>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:orientation="vertical"
ohos:padding="20vp"
>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:layout_alignment="center"
ohos:orientation="vertical"
>
<TextField
ohos:id="$+id:tf_email"
ohos:height="match_content"
ohos:width="match_parent"
ohos:background_element="$graphic:background_text_field"
ohos:hint="email"
ohos:left_padding="10vp"
ohos:min_height="40vp"
ohos:multiple_lines="false"
ohos:text_alignment="vertical_center"
ohos:text_color="black"
ohos:text_input_type="pattern_number"
ohos:text_size="15fp"/>
<Text
ohos:id="$+id:t_email_invalid"
ohos:height="match_content"
ohos:width="match_content"
ohos:layout_alignment="center"
ohos:text="invalid email"
ohos:text_color="$color:red"
ohos:text_size="15fp"
/>
</DirectionalLayout>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:layout_alignment="center"
ohos:orientation="vertical"
ohos:top_margin="10vp">
<TextField
ohos:id="$+id:tf_password"
ohos:height="match_content"
ohos:width="match_parent"
ohos:background_element="$graphic:background_text_field"
ohos:hint="password"
ohos:left_padding="10vp"
ohos:min_height="40vp"
ohos:multiple_lines="false"
ohos:text_alignment="vertical_center"
ohos:text_color="black"
ohos:text_input_type="pattern_password"
ohos:text_size="15fp"
/>
<Text
ohos:id="$+id:t_password_invalid"
ohos:height="match_content"
ohos:width="match_content"
ohos:layout_alignment="center"
ohos:padding="0vp"
ohos:text="invalid password"
ohos:text_color="$color:red"
ohos:text_size="15fp"
/>
</DirectionalLayout>
<Button
ohos:id="$+id:btn_login"
ohos:height="match_content"
ohos:width="match_parent"
ohos:background_element="$graphic:background_button"
ohos:bottom_margin="30vp"
ohos:min_height="40vp"
ohos:text="login"
ohos:text_color="#fff"
ohos:text_size="18fp"
ohos:top_margin="10vp"/>
</DirectionalLayout>
</ScrollView>
Retrofit interface
Before we move to the ViewModel, we have to setup our Retrofit service and repository class.
To keep the project clean, I will create class config.java which will hold our API URLs:
Code:
package com.megaache.mvvmdemo;
public class Config {
//todo: update base url variable with valid url
public static final String BASE_URL = "https://example.com";
public static final String API_VERSION = "/api/v1";
public static final String LOGIN_URL="auth/login";
}
Note: The url's are just for demonstration. For the demo to work, you must replace the urls.
First create interface APIServices.java:
For this tutorial, we assume the method of login EndPoint is Post, you may changes depending on your API, the method login will return an Observable, that will be observed in the ViewModel using RxJava.
Code:
package com.megaache.mvvmdemo.network;
import com.megaache.mvvmdemo.Config;
import com.megaache.mvvmdemo.network.request.LoginRequest;
import com.megaache.mvvmdemo.network.response.LoginResponse;
import io.reactivex.Observable;
import retrofit2.http.Body;
import retrofit2.http.Headers;
import retrofit2.http.POST;
public interface APIServices {
@POST(Config.LOGIN_URL)
@Headers("Content-Type: application/json;charset=UTF-8")
public Observable<LoginResponse> login(@Body LoginRequest loginRequest);
}
Note: the class LoginRequest which you will see later in this tutorial, must be equal to the request that the server expects in The names of variables and their types, otherwise the server will fail to process the request.
Then, add method createRetrofitClient() to MyApplication.java, which will create and return retrofit instance, the instance will use Moshi converter to handle the conversion of JSON to our java class, and RxJava2 adapter to return observables that can work with RxJava instead of the default Call class which requires callbacks:
Code:
package com.megaache.mvvmdemo;
import com.megaache.mvvmdemo.network.APIServices;
import ohos.aafwk.ability.AbilityPackage;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.moshi.MoshiConverterFactory;
import java.util.concurrent.TimeUnit;
public class MyApplication extends AbilityPackage {
@Override
public void onInitialize() {
super.onInitialize();
}
public static APIServices createRetrofitClient() {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60L, TimeUnit.SECONDS)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(Config.BASE_URL + Config.API_VERSION)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create()).client(client)
.build();
return retrofit.create(APIServices.class);
}
}
NOTE: For cleaner code, you can create file RetrofitClient.java and move the method createRetrofitClient() to it.
Now, let’s work on the Login feature, we going to first create request and response classes, then move to the ViewModel and the view:
We need LoginRequest and LoginResponse which both will extends BaseRequest and BaseRespnonse, code is shown below:
Create BaseRequest.java:
In real life project, your API may expect some parameter to be sent with every request. For example: accessToken, language, deviceId, pushToken etc. which will depende on your API. for this tutorial I added one field called deviceType with static value.
Code:
package com.megaache.mvvmdemo.network.request;
public class BaseRequest {
private String deviceType;
public BaseRequest() {
deviceType = "harmony-watch";
}
public String getDeviceType() {
return deviceType;
}
public void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
}
Create class LoginRequest.java, which will extend BaseRequest and have two fields Email and Password, which will provided by the end user:
Code:
package com.megaache.mvvmdemo.network.request;
public class LoginRequest {
private String email;
private String password;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Then for the response, Create BaseResponse.java first:
Code:
package com.megaache.mvvmdemo.network.response;
import com.megaache.mvvmdemo.MyApplication;
import java.io.Serializable;
public class BaseResponse implements Serializable {
}
Then LoginResponse.java extending BaseResponse:
Code:
package com.megaache.mvvmdemo.network.response;
import com.megaache.mvvmdemo.model.User;
import com.squareup.moshi.Json;
public class LoginResponse extends BaseResponse {
@Json(name = "user")
private User user;
@Json(name = "accessToken")
private String accessToken;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
}
Note: this class must be equal to the response you get from server, otherwise Retrofit Gson converter will fail to convert the response to LoginResponse class, both the type of variables and their names must equal the those in the JSON response.
ViewModel
In ViewModel, we will wrap the data which was loaded with Retrofit inside class LoggedIn in LoginViewState, and observe states Observable defined in BaseViewModel in our Ability (or AbilitySlice). Whenever the value in states changes, the ability will be notified without checking whether the ability is alive or not.
The code for LoginViewState.java extending empty class BaseViewState.java, and ErrorData.java (used in LoginViewState.java) is given below:
ErrorData.java:
Code:
package com.megaache.mvvmdemo.model;
import java.io.Serializable;
public class ErrorData implements Serializable {
private String message;
private int statusCode;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
}
LoginViewState.java:
Code:
package com.megaache.mvvmdemo.ui.login;
import com.megaache.mvvmdemo.base.BaseViewState;
import com.megaache.mvvmdemo.model.ErrorData;
import com.megaache.mvvmdemo.network.response.LoginResponse;
public class LoginViewState extends BaseViewState {
public static class Loading extends LoginViewState {
}
public static class Error extends LoginViewState {
private ErrorData message;
public Error(ErrorData message) {
this.message = message;
}
public void setMessage(ErrorData message) {
this.message = message;
}
public ErrorData getMessage() {
return message;
}
}
public static class LoggedIn extends LoginViewState {
private LoginResponse userDataResponse;
public LoggedIn(LoginResponse userDataResponse) {
this.userDataResponse = userDataResponse;
}
public LoginResponse getUserDataResponse() {
return userDataResponse;
}
public void setUserDataResponse(LoginResponse userDataResponse) {
this.userDataResponse = userDataResponse;
}
}
}
The code for the LoginViewModel.java is given below:
When the user clicks the login button, the method sendLoginRequest() will setup our retrofit Observable, the request will not be sent until the we call the method subscribe which will be done on the View. notice we are subscribing on the Schedulers.Io() Scheduler, which is will execute the requests in a background thread to avoid freezing the UI, and because of that we have to create our custom Observer that will invoke the callback code in UI thread after we receive data, more on this later:
Code:
package com.megaache.mvvmdemo.ui.login;
import com.megaache.mvvmdemo.base.BaseViewModel;
import com.megaache.mvvmdemo.MyApplication;
import com.megaache.mvvmdemo.model.ErrorData;
import com.megaache.mvvmdemo.network.request.LoginRequest;
import io.reactivex.Observable;
import io.reactivex.schedulers.Schedulers;
import ohos.aafwk.abilityjet.activedata.ActiveData;
public class LoginViewModel extends BaseViewModel<LoginViewState> {
private static final int MIN_PASSWORD_LENGTH = 6;
public ActiveData<Boolean> emailValid = new ActiveData<>();
public ActiveData<Boolean> passwordValid = new ActiveData<>();
public ActiveData<Boolean> loginState = new ActiveData<>();
public LoginViewModel() {
super();
}
public void login(String email, String password) {
boolean isEmailValid = isEmailValid(email);
emailValid.setData(isEmailValid);
if (!isEmailValid)
return;
boolean isPasswordValid = isPasswordValid(email);
passwordValid.setData(isPasswordValid);
if (!isPasswordValid)
return;
LoginRequest loginRequest = new LoginRequest();
loginRequest.setEmail(email);
loginRequest.setPassword(password);
super.subscribe(sendLoginRequest(loginRequest));
}
private Observable<LoginViewState> sendLoginRequest(LoginRequest loginRequest) {
return MyApplication.createRetrofitClient()
.login(loginRequest)
.doOnError(Throwable::printStackTrace)
.map(LoginViewState.LoggedIn::new)
.cast(LoginViewState.class)
.onErrorReturn(throwable -> {
ErrorData errorData = new ErrorData();
if (throwable.getMessage() != null)
errorData.setMessage(throwable.getMessage());
else
errorData.setMessage(" No internet! ");
return new LoginViewState.Error(errorData);
})
.subscribeOn(Schedulers.io())
.startWith(new LoginViewState.Loading());
}
private boolean isEmailValid(String email) {
return email != null && !email.isEmpty() && email.contains("@");
}
private boolean isPasswordValid(String password) {
return password != null && password.length() > MIN_PASSWORD_LENGTH;
}
}
Settings up the ability (View)
As you know, ability is our view, we have instantiated ViewModel and observer states and ActiveDatas in the method ObserverData(), as mentioned before, retrofit will send the request on background thread, therefore the code in the Observer will run on the same thread (Schedulars.io()), which will cause exceptions if that code attemp to update the UI, to prevent that, we will create a custom UIObserver class which extends Observer, that will run our code in the UI task dispatcher of the ability (UI Thread), code for UiObserver.java as show below:
Code:
package com.megaache.mvvmdemo.utils;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.abilityjet.activedata.DataObserver;
import ohos.app.dispatcher.TaskDispatcher;
public abstract class UiObserver<T> extends DataObserver<T> {
private TaskDispatcher uiTaskDispatcher;
public UiObserver(Ability baseAbilitySlice) {
setLifecycle(baseAbilitySlice.getLifecycle());
uiTaskDispatcher = baseAbilitySlice.getUITaskDispatcher();
}
@Override
public void onChanged(T t) {
uiTaskDispatcher.asyncDispatch(() -> onValueChanged(t));
}
public abstract void onValueChanged(T t);
}
Code for LoginAbility.java is shown below:
Code:
package com.megaache.mvvmdemo.ui.login;
import com.megaache.mvvmdemo.ResourceTable;
import com.megaache.mvvmdemo.utils.UiObserver;
import com.megaache.mvvmdemo.model.ErrorData;
import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.agp.components.Button;
import ohos.agp.components.Component;
import ohos.agp.components.Text;
import ohos.agp.components.TextField;
import ohos.agp.window.dialog.ToastDialog;
public class LoginAbility extends Ability {
private LoginViewModel loginViewModel;
private TextField emailTF;
private Text emailInvalidT;
private TextField passwordTF;
private Text passwordInvalidT;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
loginViewModel = new LoginViewModel();
initUI();
observeData();
}
private void initUI() {
super.setUIContent(ResourceTable.Layout_ability_login);
Button loginButton = (Button) findComponentById(ResourceTable.Id_btn_login);
loginButton.setClickedListener(c -> attemptLogin());
emailTF = (TextField) findComponentById(ResourceTable.Id_tf_email);
emailInvalidT = (Text) findComponentById(ResourceTable.Id_t_email_invalid);
passwordTF = (TextField) findComponentById(ResourceTable.Id_tf_password);
passwordInvalidT = (Text) findComponentById(ResourceTable.Id_t_password_invalid);
}
private void observeData() {
loginViewModel.emailValid.addObserver(new UiObserver<Boolean>(this) {
@Override
public void onValueChanged(Boolean aBoolean) {
emailInvalidT.setVisibility(aBoolean ? Component.VISIBLE : Component.HIDE);
}
}, false);
loginViewModel.passwordValid.addObserver(new UiObserver<Boolean>(this) {
@Override
public void onValueChanged(Boolean aBoolean) {
passwordInvalidT.setVisibility(aBoolean ? Component.VISIBLE : Component.HIDE);
}
}, false);
loginViewModel.getStates().addObserver(new UiObserver<LoginViewState>(this) {
@Override
public void onValueChanged(LoginViewState loginState) {
if (loginState instanceof LoginViewState.Loading) {
toggleLoadingDialog(true);
} else if (loginState instanceof LoginViewState.Error) {
toggleLoadingDialog(false);
manageError(((LoginViewState.Error) loginState).getMessage());
} else if (loginState instanceof LoginViewState.LoggedIn) {
toggleLoadingDialog(false);
showToast("logging successful!");
}
}
}, false);
}
private void attemptLogin() {
loginViewModel.login(emailTF.getText(), passwordTF.getText());
}
private void toggleLoadingDialog(boolean show) {
//todo: show/hide loading dialog
}
private void manageError(ErrorData errorData) {
showToast(errorData.getMessage());
}
private void showToast(String message) {
new ToastDialog(this)
.setText(message)
.show();
}
@Override
protected void onStop() {
super.onStop();
loginViewModel.unbind();
}
}
Tips And Tricks
If you want your app to work offline, its best to introduce a Repository classes that will handle quering information from server if internet is available or from the cach if not
For cleaner code, try re-using the ViewModel as much as possible, by creating a base class and moving the shared code their
You should not keep a reference to a View (component) or context in the ViewModel, unless you have no option
The ViewModel should not talk directly to the View, instead the View obseve the ViewModel and update itself depending on ViewModel data
A correct implementation of ViewModel should allow you to change the UI with minimal or zero changes to the ViewModel.
Conclusion
MVVM combines the advantages of separation of concerns provided by MVP archichetecture, while leveraging the advantages of RxJava or Data binding. The result is a pattern where the model drives as many of the operations as possible, minimizing the logic in the view.
Finally, talk is cheap, and I strongly advise you to try and learn these things in the code so that you do not need to rely on people like me to tell you what to do.
Clone the project from github, replate the API Urls in class config.java and run it on HarmonyOs Device, you should see a toast that says "Logging succesful" if the credentials are correct, otherwise it should show a toast with the error that says "no internet" or and error returned from the server.
This project is available on GitHub: Click here
Recommended sources:
HarmonyOS (essential topics): Essential Topics
Retrofit: Click here
Rxjava: Click here
Original Source
Comment below if you have any questions or suggestions.
Thank you!
In Android we have Manifest file right there we will declare all the activity names like that do we have any file here?