Integrating Rust + Flutter + Kotlin for Mobile Applications - android

As next week will have importat launch for Rust 2018 and Flutter 1.0, I thought to build an app using Rust for the business logic and Flutter for the user interface, that can run at both Android and iOS, I built one and tested it at Android and it is working fine.
I just wonder how to measure the performance and compare it with native Android/iOS app.
The app flow is:
Main is in Flutter, that is calling native function through platform_channel
The native function is calling rust library through JNI (JNI wrapper is required to be call the rust library)
The structure is as below:
The code used is:
main.dart:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('samples.flutter.io/battery');
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final String hello = await platform.invokeMethod('getText');
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = '$hello Battery level at $result %.';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
#override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('Get Battery Level'),
onPressed: _getBatteryLevel,
),
Text(_batteryLevel),
],
),
),
);
}
}
JNI wrapper - RustGreetings.kt
package com.mozilla.greetings
class RustGreetings {
companion object {
init {
System.loadLibrary("greetings")
}
}
private external fun greeting(pattern: String): String
fun sayHello(to: String): String = greeting(to)
}
And the Main Android activity is:
package com.example.batterylevel
import android.os.Bundle
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import lib.Library
import com.mozilla.greetings.RustGreetings
class MainActivity: FlutterActivity() {
private val CHANNEL = "samples.flutter.io/battery"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getText") {
result.success(getText())
} else if (call.method == "getBatteryLevel") {
// result.success(getText())
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
}
else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryLevel: Int
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
} else {
val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
}
return batteryLevel
}
private fun getText(): String {
val x = Library().someLibraryMethod()
val g = RustGreetings()
val r = g.sayHello("My $x Rust")
return r
}
}
In the Android gradle.build I just added the below, as I'm interested to check also the impact of adding kotlin JVM library and getting it interacted with the Rust library within the mobile application:
dependencies {
implementation(files("src/main/libs/lib.jar"))
}
My question is:
How can check the performance and impact of each process when it is executed or called by another process

With the introduction of ffi in Dart, things became more smoother now, with a better performance as the interction now is Dart/Rust directly, without a need for Dart/Kotlin/Rust or Dart/Swift/Rust cycle, below a simple example:
First src/lib.rs
#[no_mangle]
pub extern fn rust_fn(x: i32) -> i32 {
println!("Hello from rust\nI'll return: {}", x.pow(2));
x.pow(2)
}
and Cargo.toml
[package]
name = "Double_in_Rost"
version = "0.1.0"
authors = ["Hasan Yousef"]
edition = "2018"
[lib]
name = "rust_lib"
crate-type = ["dylib"] # could be `staticlib` as well
[dependencies]
Running cargo build --release will generate target\release\rust_lib.dll copy/paste it into Dart application root directory
Write Dart code as below:
import 'dart:ffi';
import 'dart:io' show Platform;
// FFI signature of the hello_world C function
typedef ffi_func = Int32 Function(Int32 x); //pub extern fn rust_fn(x: i32) -> i32
// Dart type definition for calling the C foreign function
typedef dart_func = int Function(int x);
void main() {
// Open the dynamic library
var path = './rust_lib.so';
if (Platform.isMacOS) path = './rust_lib.dylib';
if (Platform.isWindows) path = 'rust_lib.dll';
final dylib = DynamicLibrary.open(path);
// Look up the Rust/C function
final my_func =
dylib.lookup<NativeFunction<ffi_func>>('rust_fn').asFunction<dart_func>();
print('Double of 3 is ${my_func(3)}');
}

Related

How to fetch api and send response as notification in flutter?

enter image description hereI want to fetch api and send response as notification everyday, even when my flutter app is not opened. So, I use flutter local notification and flutter workmanager packages. But it can not exactly works. **Here is my main.dart code : **
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:message_app/constants/constants.dart';
import 'package:message_app/controller_bindings.dart';
import 'package:message_app/routes.dart';
import 'package:message_app/screens/welcome_screen.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:message_app/services/getx_api_services.dart';
import 'package:shurjopay/utilities/functions.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart';
import 'package:workmanager/workmanager.dart';
const simplePeriodicTask = "simplePeriodicTask";
// flutter local notification setup
void _showNotificationWithDefaultSound(v, flp) async {
var androidPlatformChannelSpecifics = AndroidNotificationDetails(
'channel id', 'channel NAME',
channelDescription: 'CHANNEL DESCRIPTION',
priority: Priority.high,
importance: Importance.max);
// var iOS = IOSNotificationDetails();
var platform = NotificationDetails(android: androidPlatformChannelSpecifics);
await flp.show(0, 'Message', '$v', platform, payload: 'Loading!!');
}
Future main() async {
await dotenv.load();
initializeShurjopay(environment: "live");
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(statusBarColor: Constants.primaryTealDark),
);
WidgetsFlutterBinding.ensureInitialized();
await Workmanager().initialize(callbackDispatcher,
isInDebugMode:
true); //to true if still in testing lev turn it to false whenever you are launching the app
await Workmanager().registerPeriodicTask("5", simplePeriodicTask,
existingWorkPolicy: ExistingWorkPolicy.replace,
frequency: Duration(minutes: 15), //when should it check the link
initialDelay:
Duration(seconds: 5), //duration before showing the notification
constraints: Constraints(
networkType: NetworkType.connected,
));
runApp(const MyApp());
}
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
FlutterLocalNotificationsPlugin flp = FlutterLocalNotificationsPlugin();
var android = AndroidInitializationSettings('#mipmap/ic_launcher');
// var iOS = IOSInitializationSettings();
var initSetttings = InitializationSettings(android: android);
flp.initialize(initSetttings);
GetXApiServices api = GetXApiServices();
api
.fetchDailyAyah((api.mon % 3) == 0
? api.num * api.mon + api.year - 2000
: api.num * api.mon + api.year + 500)
.then((_) {
_showNotificationWithDefaultSound(flp, api.ayah.last.arText);
});
return Future.value(true);
});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
return GetMaterialApp(
title: 'Message',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.teal,
primaryColor: Colors.teal.shade700,
scaffoldBackgroundColor: Colors.white70,
),
// initialRoute: '/',
routes: customRoutes,
initialBinding: ControllerBindings(),
home: const WelcomeScreen(),
);
}
}
And here is my getx_api_services.dart code:
import 'package:message_app/constants/constants.dart';
import 'package:message_app/models/aya_of_the_day.dart';
import 'package:http/http.dart' as http;
import 'package:message_app/models/surah.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:message_app/models/prayer_time.dart';
class GetXApiServices extends GetxController {
int num = int.parse(DateFormat('d').format(DateTime.now()));
int mon = int.parse(DateFormat('MM').format(DateTime.now()));
int year = int.parse(DateFormat('yyyy').format(DateTime.now()));
#override
void onInit() async {
super.onInit();
await fetchDailyAyah(
(mon % 3) == 0 ? num * mon + year - 2000 : num * mon + year + 500);
}
List<Surah> surahList = [];
List<Surah> msgSurah = [
Surah(
number: 0,
name: " ",
englishName: " ",
englishNameTranslation: " ",
numberofAyahs: 0,
revelationType: " ",
),
];
var isLoading = true.obs;
var ayah = <AyaOfTheDay>[].obs;
var prayerTime = <PrayerTime>[].obs;
var bhasha = false.obs;
// late Rx<AyaOfTheDay> ayahlatest;
Future<void> fetchDailyAyah(int day) async {
String url = "http://api.alquran.cloud/v1/ayah/$day/editions/bn.bengali";
String enUrl = "https://api.alquran.cloud/v1/ayah/$day/editions/en.asad";
String urlforSurah = "http://api.alquran.cloud/v1/surah";
final response = await http.get(Uri.parse(url));
final enResponse = await http.get(Uri.parse(enUrl));
if (response.statusCode == 200 && enResponse.statusCode == 200) {
AyaOfTheDay albumModel = AyaOfTheDay.fromJson(jsonDecode(response.body));
AyaOfTheDay albumModelEn =
AyaOfTheDay.fromJson(jsonDecode(enResponse.body));
final res = await http.get(Uri.parse(urlforSurah));
if (response.statusCode == 200 && res.statusCode == 200) {
// print("from API Call");
Map<String, dynamic> jsonSurah = jsonDecode(res.body);
jsonSurah['data'].forEach((element) {
if (surahList.length < 114) {
surahList.add(Surah.fromJson(element));
}
});
Constants.surahBackup = surahList;
ayah.add(
AyaOfTheDay(
arText: albumModel.arText,
surEnName: albumModel.surEnName,
surahSerial: albumModel.surahSerial,
surNumber: albumModel.surNumber,
revelationType: albumModel.revelationType,
),
);
ayah.add(
AyaOfTheDay(
arText: albumModelEn.arText,
surEnName: albumModelEn.surEnName,
surahSerial: albumModelEn.surahSerial,
surNumber: albumModelEn.surNumber,
revelationType: albumModelEn.revelationType,
),
);
isLoading.value = false;
update();
} else {
Get.snackbar('Error Loading data!',
'Sever responded: ${response.statusCode}:${response.reasonPhrase.toString()}');
}
}
}
}
**here is my debug console log: **
Building with Flutter multidex support enabled.
√ Built build\app\outputs\flutter-apk\app-debug.apk.
D/FlutterGeolocator(10751): Attaching Geolocator to activity
D/FlutterGeolocator(10751): Creating service.
D/FlutterGeolocator(10751): Binding to location service.
D/FlutterLocationService(10751): Creating service.
D/FlutterLocationService(10751): Binding to location service.
D/FlutterGeolocator(10751): Geolocator foreground service connected
D/FlutterGeolocator(10751): Initializing Geolocator services
D/FlutterGeolocator(10751): Flutter engine connected. Connected engine count 1
I/ple.message_ap(10751): ProcessProfilingInfo new_methods=2499 is saved saved_to_disk=1 resolve_classes_delay=5000
I/ple.message_ap(10751): Compiler allocated 4670KB to compile void android.view.ViewRootImpl.performTraversals()
Connecting to VM Service at ws://127.0.0.1:4096/-u3EjpN6CA4=/ws
[GETX] Instance "GetXApiServices" has been created
[GETX] Instance "GetXApiServices" has been initialized
[GETX] Instance "GetMaterialController" has been created
[GETX] Instance "GetMaterialController" has been initialized
E/ion (10751): ioctl c0044901 failed with code -1: Invalid argument
E/libEGL (10751): Invalid file path for libcolorx-loader.so
I/chatty (10751): uid=10842(com.example.message_app) identical 1 line
E/libEGL (10751): Invalid file path for libcolorx-loader.so
E/libEGL (10751): Invalid file path for libcolorx-loader.so
I/chatty (10751): uid=10842(com.example.message_app) 2.io identical 58 lines
E/libEGL (10751): Invalid file path for libcolorx-loader.so
D/FlutterGeolocator(10751): Geolocator foreground service connected
D/FlutterGeolocator(10751): Initializing Geolocator services
D/FlutterGeolocator(10751): Flutter engine connected. Connected engine count 2
W/FlutterJNI(10751): FlutterJNI.loadLibrary called more than once
W/FlutterJNI(10751): FlutterJNI.prefetchDefaultFontManager called more than once
I/ResourceExtractor(10751): Found extracted resources res_timestamp-1-1674402227534
W/FlutterJNI(10751): FlutterJNI.init called more than once
D/FlutterGeolocator(10751): Flutter engine disconnected. Connected engine count 1
D/FlutterGeolocator(10751): Disposing Geolocator services
E/FlutterGeolocator(10751): Geolocator position updates stopped
E/FlutterGeolocator(10751): There is still another flutter engine connected, not stopping location service
I/WM-WorkerWrapper(10751): Worker result SUCCESS for Work [ id=121d6b93-46cb-49a7-b71c-98c31114d5c8, tags={ be.tramckrijte.workmanager.BackgroundWorker } ]
I/GED (10751): ged_boost_gpu_freq, level 100, eOrigin 2, final_idx 31, oppidx_max 31, oppidx_min 0
I can not Understand why it is not working.
here is also my app notification screenshot:

Sleepless process on Ionic5, Capacitor, Android

I have an ionic5 app with Capacitor that I'd like to deploy on android. When the app starts I'm getting a list of reminders (from API) I'd like to schedule to be shown as local notifications during the day. After deploying it to my device (by .apk file) is working fine when it's open but after some time when the phone is not in use it's getting sleep and no notification appears. What would be the best way to solve that case? This is my last code with BackgroundTask but it doesn't work anyway.
import { Injectable } from "#angular/core";
import { Job } from "src/entities/respons/_respons";
import { Plugins } from '#capacitor/core';
const { LocalNotifications, BackgroundTask } = Plugins;
#Injectable({
providedIn: 'root',
})
export class NotificationsService {
constructor() {
LocalNotifications.requestPermission();
console.log(` Initialized on ${new Date().toLocaleString()}`);
}
async setup(jobs: Job[]) {
let id: number = 0;
let taskId = BackgroundTask.beforeExit(async () => {
let toSchedule = jobs.filter(e => (e.isActive || e.isFuture) && !e.isNotified);
id = toSchedule.length;
console.log(`Setup, count ${id};`)
let notificationInterval = setInterval(async () => {
let toNotify = jobs.filter(e => e.isActive && !e.isNotified);
if (toNotify.length > 0) {
let logger: string = '';
toNotify.forEach(async job => {
let d = new Date(job.since);
logger += `[${d.toLocaleTimeString()} ${job.name}], `;
await LocalNotifications.schedule({
notifications: [
{
id: job.id,
title: `${job.name} ${d.toLocaleTimeString()}`,
body: job.body,
iconColor: '#0081ca'
}
]
});
job.isNotified = true;
id--;
});
console.log(`Tick on ${new Date(Date.now()).toTimeString()} , count ${toNotify.length}; Scheduled: ${logger}`)
}
if (id <= 0) {
console.log(`Task finished ${id}; no.:${taskId}`);
clearInterval(notificationInterval);
BackgroundTask.finish({
taskId
});
} else {
console.log(`Task in progress ${id}; no.:${taskId}`,)
}
}, 60000)
});
}
}

How to remove the blank white screen in android phone in react native project?

I am new to react-native.
I am making a project in react-native with my android phone connected to my laptop at the USB port. When I run the project I see a blank screen on my phone. My phone is android version 9.
First I ran npm start and then I ran npm run android.
I ran adb -s device-name reverse tcp:8081 tcp:8081. Still the screen on my phone is blank.
This is my App.tsx file:
import "./i18n"
import React, { useState, useEffect, useRef } from "react"
import { YellowBox } from "react-native"
import { NavigationContainerRef } from "#react-navigation/native";
import { contains } from "ramda"
import { enableScreens } from "react-native-screens"
import { SafeAreaProvider, initialWindowSafeAreaInsets } from "react-native-safe-area-context";
import { RootNavigator, exitRoutes, setRootNavigation } from "./navigation"
import { useBackButtonHandler } from "./navigation/use-back-button-handler"
import { RootStore, RootStoreProvider, setupRootStore } from "./models/root-store"
import * as storage from "./utils/storage"
import getActiveRouteName from "./navigation/get-active-routename"
import { ThemeProvider } from 'react-native-elements';
import * as theme from 'theme';
enableScreens()
YellowBox.ignoreWarnings([
"componentWillMount is deprecated",
"componentWillReceiveProps is deprecated",
"Require cycle:",
])
const canExit = (routeName: string) => contains(routeName, exitRoutes)
export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE"
const App: React.FunctionComponent<{}> = () => {
const navigationRef = useRef<NavigationContainerRef>()
const [rootStore, setRootStore] = useState<RootStore | undefined>(undefined)
const [initialNavigationState, setInitialNavigationState] = useState()
const [isRestoringNavigationState, setIsRestoringNavigationState] = useState(true)
setRootNavigation(navigationRef)
useBackButtonHandler(navigationRef, canExit)
const routeNameRef = useRef()
const onNavigationStateChange = state => {
const previousRouteName = routeNameRef.current
const currentRouteName = getActiveRouteName(state)
if (previousRouteName !== currentRouteName) {
// track screens.
__DEV__ && console.tron.log(currentRouteName)
}
// Save the current route name for later comparision
routeNameRef.current = currentRouteName
// Clear the storage if we are navigating to auth stack
if ( ['register', 'login', 'forgotpassword'].includes(currentRouteName) ) {
storage.remove(NAVIGATION_PERSISTENCE_KEY);
} else {
// Persist navigation state to storage
storage.save(NAVIGATION_PERSISTENCE_KEY, state)
}
}
useEffect(() => {
(async () => {
setupRootStore().then(setRootStore)
})()
}, [])
useEffect(() => {
const restoreState = async () => {
try {
const state = await storage.load(NAVIGATION_PERSISTENCE_KEY)
if (state) {
setInitialNavigationState(state)
}
} finally {
setIsRestoringNavigationState(false)
}
}
if (isRestoringNavigationState) {
restoreState()
}
}, [isRestoringNavigationState])
if (!rootStore) {
return null
}
return (
<RootStoreProvider value={rootStore}>
<SafeAreaProvider initialSafeAreaInsets={initialWindowSafeAreaInsets}>
<ThemeProvider theme={theme}>
<RootNavigator
ref={navigationRef}
initialState={initialNavigationState}
onStateChange={onNavigationStateChange}
/>
</ThemeProvider>
</SafeAreaProvider>
</RootStoreProvider>
)
}
export default App
Can anyone help?

Intergrating native Twilio Android SDK with Flutter

I am trying to create Voice Over IP (VOIP) mobile application using flutter.I haven't seen an implementation for a flutter plugin for twilio voice api so i intergrated my application with the native android voice api using MethodChannel.The twilio SDK doesnt seem like it intergrated correctly i cant access the twilio classes and methods in scripts. These are the errors i get.
Running Gradle task 'assembleDebug'...
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:23: error: package android.support.annotation does not exist
import android.support.annotation.NonNull;
^
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:295: error: cannot find symbol
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
^
symbol: class NonNull
location: class MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:295: error: cannot find symbol
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
^
symbol: class NonNull
location: class MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:117: error: cannot find symbol
soundPoolManager = SoundPoolManager.getInstance(this.MainActivity);
^
symbol: variable MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:186: error: cannot find symbol
public void onReconnecting(#NonNull Call call, #NonNull CallException callException) {
^
symbol: class NonNull
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:186: error: cannot find symbol
public void onReconnecting(#NonNull Call call, #NonNull CallException callException) {
^
symbol: class NonNull
/home/kudziesimz/voip20/android/app/src/main/java /com/workerbees/voip20/MainActivity.java:191: error: cannot find symbol
public void onReconnected(#NonNull Call call) {
^
symbol: class NonNull
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:279: error: cannot find symbol
int resultMic = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
^
symbol: variable ContextCompat
location: class MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:284: error: method shouldShowRequestPermissionRationale in class Activity cannot be applied to given types;
if (MainActivity.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
^
required: String
found: MainActivity,String
reason: actual and formal argument lists differ in length
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:287: error: method requestPermissions in class Activity cannot be applied to given types;
MainActivity.requestPermissions(
^
required: String[],int
found: MainActivity,String[],int
reason: actual and formal argument lists differ in length
Note: /home/kudziesimz/voip20/android/app/src/main/java /com/workerbees/voip20/MainActivity.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
10 errors
I followed the voice-quickstart-android guide shown here https://github.com/twilio/voice-quickstart-android
here is my code:main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
//This is a test application which allows clients to make Voice Over The Internet Cal
void main() => runApp(MaterialApp(
home: MyApp(),
));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
static const platform = const MethodChannel("com.voip.call_management/calls");
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Call Management"),
),
bottomNavigationBar: Center(
child: IconButton(
icon: Icon(Icons.phone),
onPressed: () {
_makeCall;
}),
),
);
}
Future<void> _makeCall() async {
return showDialog<void>(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: Row(
children: <Widget>[
Text('Call'),
Icon(
Icons.phone,
color: Colors.blue,
)
],
),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
TextField(
decoration: InputDecoration(
hintText: "client identity or phone number"),
),
SizedBox(
height: 20,
),
Text(
'Dial a client name or number.Leaving the field empty will result in an automated response.'),
],
),
),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
IconButton(icon: Icon(Icons.phone), onPressed:()async {
try {
final result = await platform.invokeMethod("makecall");
} on PlatformException catch (e) {
print(e.message);
}
})
],
);
},
);
}
}
MainActivity.java
package com.workerbees.voip20;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
//javacode imports
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.firebase.iid.FirebaseInstanceId;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.Ion;
import com.twilio.voice.Call;
import com.twilio.voice.CallException;
import com.twilio.voice.CallInvite;
import com.twilio.voice.ConnectOptions;
import com.twilio.voice.RegistrationException;
import com.twilio.voice.RegistrationListener;
import com.twilio.voice.Voice;
import java.util.HashMap;
//sound pool imports
import android.media.SoundPool;
import static android.content.Context.AUDIO_SERVICE;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.workerbees.voip/calls"; // MethodChannel Declaration
private static final String TAG = "VoiceActivity";
private static String identity = "alice";
private static String contact;
/*
* You must provide the URL to the publicly accessible Twilio access token server route
*
* For example: https://myurl.io/accessToken
*
* If your token server is written in PHP, TWILIO_ACCESS_TOKEN_SERVER_URL needs .php extension at the end.
*
* For example : https://myurl.io/accessToken.php
*/
private static final String TWILIO_ACCESS_TOKEN_SERVER_URL = "https://bd107744.ngrok.io/accessToken";
private static final int MIC_PERMISSION_REQUEST_CODE = 1;
private String accessToken;
private AudioManager audioManager;
private int savedAudioMode = AudioManager.MODE_INVALID;
// Empty HashMap, never populated for the Quickstart
HashMap<String, String> params = new HashMap<>();
private SoundPoolManager soundPoolManager;
private Call activeCall;
Call.Listener callListener = callListener();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
#Override
public void onMethodCall(MethodCall call, Result result) {
// Note: this method is invoked on the main thread.
// TODO
if(call.method.equals("makecall")){
params.put("to", contact);
ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken)
.params(params)
.build();
activeCall = Voice.connect(MainActivity.this, connectOptions, callListener);
}
else if(call.method.equals("hangup")){
disconnect();
}
else if(call.method.equals("mute")){
mute();
}
else if (call.method.equals("hold")){
hold();
}
else{
Log.d(TAG,"invalid API call");
}
}
});
soundPoolManager = SoundPoolManager.getInstance(this.MainActivity);
/*
* Needed for setting/abandoning audio focus during a call
*/
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.setSpeakerphoneOn(true);
/*
* Enable changing the volume using the up/down keys during a conversation
*/
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
/*
* Displays a call dialog if the intent contains a call invite
*/
//handleIncomingCallIntent(getIntent());
/*
* Ensure the microphone permission is enabled
*/
if (!checkPermissionForMicrophone()) {
requestPermissionForMicrophone();
} else {
retrieveAccessToken();
}
}
private Call.Listener callListener() {
return new Call.Listener() {
/*
* This callback is emitted once before the Call.Listener.onConnected() callback when
* the callee is being alerted of a Call. The behavior of this callback is determined by
* the answerOnBridge flag provided in the Dial verb of your TwiML application
* associated with this client. If the answerOnBridge flag is false, which is the
* default, the Call.Listener.onConnected() callback will be emitted immediately after
* Call.Listener.onRinging(). If the answerOnBridge flag is true, this will cause the
* call to emit the onConnected callback only after the call is answered.
* See answeronbridge for more details on how to use it with the Dial TwiML verb. If the
* twiML response contains a Say verb, then the call will emit the
* Call.Listener.onConnected callback immediately after Call.Listener.onRinging() is
* raised, irrespective of the value of answerOnBridge being set to true or false
*/
#Override
public void onRinging(Call call) {
Log.d(TAG, "Ringing");
}
#Override
public void onConnectFailure(Call call, CallException error) {
setAudioFocus(false);
Log.d(TAG, "Connect failure");
String message = String.format("Call Error: %d, %s", error.getErrorCode(), error.getMessage());
Log.e(TAG, message);
}
#Override
public void onConnected(Call call) {
setAudioFocus(true);
Log.d(TAG, "Connected");
activeCall = call;
}
#Override
public void onReconnecting(#NonNull Call call, #NonNull CallException callException) {
Log.d(TAG, "onReconnecting");
}
#Override
public void onReconnected(#NonNull Call call) {
Log.d(TAG, "onReconnected");
}
#Override
public void onDisconnected(Call call, CallException error) {
setAudioFocus(false);
Log.d(TAG, "Disconnected");
if (error != null) {
String message = String.format("Call Error: %d, %s", error.getErrorCode(), error.getMessage());
Log.e(TAG, message);
}
}
};
}
private void disconnect() {
if (activeCall != null) {
activeCall.disconnect();
activeCall = null;
}
}
private void hold() {
if (activeCall != null) {
boolean hold = !activeCall.isOnHold();
activeCall.hold(hold);
}
}
private void mute() {
if (activeCall != null) {
boolean mute = !activeCall.isMuted();
activeCall.mute(mute);
}
}
private void setAudioFocus(boolean setFocus) {
if (audioManager != null) {
if (setFocus) {
savedAudioMode = audioManager.getMode();
// Request audio focus before making any device switch.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(new AudioManager.OnAudioFocusChangeListener() {
#Override
public void onAudioFocusChange(int i) {
}
})
.build();
audioManager.requestAudioFocus(focusRequest);
} else {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.FROYO) {
int focusRequestResult = audioManager.requestAudioFocus(
new AudioManager.OnAudioFocusChangeListener() {
#Override
public void onAudioFocusChange(int focusChange)
{
}
}, AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
}
}
/*
* Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
* required to be in this mode when playout and/or recording starts for
* best possible VoIP performance. Some devices have difficulties with speaker mode
* if this is not set.
*/
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
} else {
audioManager.setMode(savedAudioMode);
audioManager.abandonAudioFocus(null);
}
}
}
private boolean checkPermissionForMicrophone() {
int resultMic = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
return resultMic == PackageManager.PERMISSION_GRANTED;
}
private void requestPermissionForMicrophone() {
if (MainActivity.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
} else {
MainActivity.requestPermissions(
this,
new String[]{Manifest.permission.RECORD_AUDIO},
MIC_PERMISSION_REQUEST_CODE);
}
}
#Override
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
/*
* Check if microphone permissions is granted
*/
if (requestCode == MIC_PERMISSION_REQUEST_CODE && permissions.length > 0) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Microphone permissions needed. Please allow in your application settings.");
} else {
retrieveAccessToken();
}
}
}
/*
* Get an access token from your Twilio access token server
*/
private void retrieveAccessToken() {
Ion.with(this).load(TWILIO_ACCESS_TOKEN_SERVER_URL + "?identity=" + identity).asString().setCallback(new FutureCallback<String>() {
#Override
public void onCompleted(Exception e, String accessToken) {
if (e == null) {
Log.d(TAG, "Access token: " + accessToken);
MainActivity.this.accessToken = accessToken;
} else {
Log.d(TAG, "Registration failed");
}
}
});
}
}
class SoundPoolManager {
private boolean playing = false;
private boolean loaded = false;
private boolean playingCalled = false;
private float actualVolume;
private float maxVolume;
private float volume;
private AudioManager audioManager;
private SoundPool soundPool;
private int ringingSoundId;
private int ringingStreamId;
private int disconnectSoundId;
private static SoundPoolManager instance;
private SoundPoolManager(Context context) {
// AudioManager audio settings for adjusting the volume
audioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
actualVolume = (float) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
maxVolume = (float) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
volume = actualVolume / maxVolume;
// Load the sounds
int maxStreams = 1;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
soundPool = new SoundPool.Builder()
.setMaxStreams(maxStreams)
.build();
} else {
soundPool = new SoundPool(maxStreams, AudioManager.STREAM_MUSIC, 0);
}
soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
#Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
loaded = true;
if (playingCalled) {
playRinging();
playingCalled = false;
}
}
});
ringingSoundId = soundPool.load(context, R.raw.incoming, 1);
disconnectSoundId = soundPool.load(context, R.raw.disconnect, 1);
}
public static SoundPoolManager getInstance(Context context) {
if (instance == null) {
instance = new SoundPoolManager(context);
}
return instance;
}
public void playRinging() {
if (loaded && !playing) {
ringingStreamId = soundPool.play(ringingSoundId, volume, volume, 1, -1, 1f);
playing = true;
} else {
playingCalled = true;
}
}
public void stopRinging() {
if (playing) {
soundPool.stop(ringingStreamId);
playing = false;
}
}
public void playDisconnect() {
if (loaded && !playing) {
soundPool.play(disconnectSoundId, volume, volume, 1, 0, 1f);
playing = false;
}
}
public void release() {
if (soundPool != null) {
soundPool.unload(ringingSoundId);
soundPool.unload(disconnectSoundId);
soundPool.release();
soundPool = null;
}
instance = null;
}
}
This my build.gradle
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle /flutter.gradle"
android {
compileSdkVersion 28
lintOptions {
disable 'InvalidPackage'
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.workerbees.voip20"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
// Specify that we want to split up the APK based on ABI
splits {
abi {
// Enable ABI split
enable true
// Clear list of ABIs
reset()
// Specify each architecture currently supported by the Video SDK
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
// Specify that we do not want an additional universal SDK
universalApk false
}
}
}
}
flutter {
source '../..'
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation 'com.twilio:voice-android:4.5.0'
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support:support-media-compat:28.0.0'
implementation 'com.android.support:animated-vector-drawable:28.0.0'
implementation 'com.android.support:support-v4:28.0.0'
implementation 'com.squareup.retrofit:retrofit:1.9.0'
implementation 'com.koushikdutta.ion:ion:2.1.8'
implementation 'com.google.firebase:firebase-messaging:17.6.0'
implementation 'com.android.support:support-annotations:28.0.0'
}
this is my build.gradle from my gradle folder
buildscript {
repositories {
jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven {
url 'https://maven.google.com/'
name 'Google'
}
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Are you still having this issue? Following Flutter platform channels guide, I was able to use Twilio Android SDK without issues. I integrated the bare minimum components needed for Twilio in this demo based from Twilio's Android quickstart.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('samples.flutter.dev/twilio');
Future<void> callTwilio() async{
try {
final String result = await platform.invokeMethod('callTwilio');
debugPrint('Result: $result');
} on PlatformException catch (e) {
debugPrint('Failed: ${e.message}.');
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Hello',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => callTwilio(),
tooltip: 'Call',
child: Icon(Icons.phone),
),
);
}
}
android/app/src/main/kotlin/{PACKAGE_NAME}/MainActivity.kt
class MainActivity : FlutterActivity() {
private val CHANNEL = "samples.flutter.dev/twilio"
private val TAG = "MainActivity"
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "callTwilio") {
executeTwilioVoiceCall()
result.success("Hello from Android")
} else {
result.notImplemented()
}
}
}
private val accessToken = ""
var params = HashMap<String, String>()
var callListener: Call.Listener = callListener()
fun executeTwilioVoiceCall(){
val connectOptions = ConnectOptions.Builder(accessToken)
.params(params)
.build()
Voice.connect(this, connectOptions, callListener)
}
private fun callListener(): Call.Listener {
return object : Call.Listener {
override fun onRinging(call: Call) {
Log.d(TAG, "Ringing")
}
override fun onConnectFailure(call: Call, error: CallException) {
Log.d(TAG, "Connect failure")
}
override fun onConnected(call: Call) {
Log.d(TAG, "Connected")
}
override fun onReconnecting(call: Call, callException: CallException) {
Log.d(TAG, "onReconnecting")
}
override fun onReconnected(call: Call) {
Log.d(TAG, "onReconnected")
}
override fun onDisconnected(call: Call, error: CallException?) {
Log.d(TAG, "Disconnected")
}
override fun onCallQualityWarningsChanged(call: Call,
currentWarnings: MutableSet<CallQualityWarning>,
previousWarnings: MutableSet<CallQualityWarning>) {
if (previousWarnings.size > 1) {
val intersection: MutableSet<CallQualityWarning> = HashSet(currentWarnings)
currentWarnings.removeAll(previousWarnings)
intersection.retainAll(previousWarnings)
previousWarnings.removeAll(intersection)
}
val message = String.format(
Locale.US,
"Newly raised warnings: $currentWarnings Clear warnings $previousWarnings")
Log.e(TAG, message)
}
}
}
}
As for the dependencies in Android, I've added these on the build.gradle configs
android/build.gradle
ext.versions = [
'voiceAndroid' : '5.6.2',
'audioSwitch' : '1.1.0',
]
android/app/build.grade
dependencies {
...
implementation "com.twilio:audioswitch:${versions.audioSwitch}"
implementation "com.twilio:voice-android:${versions.voiceAndroid}"
}
Here's my flutter doctor verbose logs for reference
[✓] Flutter (Channel master, 1.26.0-2.0.pre.281, on macOS 11.1 20C69 darwin-x64)
• Flutter version 1.26.0-2.0.pre.281
• Framework revision 4d5db88998 (3 weeks ago), 2021-01-11 10:29:26 -0800
• Engine revision d5cacaa3a6
• Dart version 2.12.0 (build 2.12.0-211.0.dev)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Platform android-30, build-tools 29.0.2
• Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 12.0.1)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Xcode 12.0.1, Build version 12A7300
• CocoaPods version 1.10.0
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 4.1)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
[✓] VS Code (version 1.52.1)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.18.1
[✓] Connected device (2 available)
• AOSP on IA Emulator (mobile) • emulator-5554 • android-x86 • Android 9 (API 28) (emulator)
• Chrome (web) • chrome • web-javascript • Google Chrome 88.0.4324.96
• No issues found!
Here's how the sample app looks when run. Logs throw "Connect failure" and "Forbidden:403" errors since the API keys set are invalid, but this proves that Twilio Android SDK is functional through Flutter platform channels.
You can also check pub.dev for Twilio Flutter plugins made by the community that may fit your use case.

Can I call Kotlin function from Dart

Using Flutter, a kotlin/swift function can be called by something like:
file.dart:
static const platform = const MethodChannel('my.test.flutterapp/battery');
final int result = await platform.invokeMethod('getBatteryLevel');
file.kt:
private val CHANNEL = "my.test.flutterapp/battery"
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
...
} else {
result.notImplemented()
}
}
Is there something similar to call Kotlin function from standard Dart app, like Dart console app!
https://flutter.io/developing-packages/
Plugin packages: A specialized Dart package which contain an API written in Dart code combined with a platform-specific implementation for Android (using Java or Kotlin), and/or for iOS (using ObjC or Swift). A concrete example is the battery plugin package.
...
flutter create --template=plugin -i swift -a kotlin hello
For the VM the mechanisms available are basic OS operations and native extensions.
By OS operations I mean, you could launch a separate process and interact with it, using files or the network stack. This is probably not as fine grained as you're looking for.
Native extensions allow you to call out to C or C++ code. I don't know enough about kotlin to know if you can easily expose functionality to C/C++. If it's possible, this will give you the tightest integration.
https://www.dartlang.org/articles/dart-vm/native-extensions
You can see this project: Full Sample
In Andriod:
class MainActivity: FlutterActivity() {
private val DATA_CHANNEL = "app.channel.shared.data"
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), DATA_CHANNEL).setMethodCallHandler { call, result ->
if (call.method!!.contentEquals("getSharedText")) {
result.success("Shared Text")
}
}
}
}
In Dart:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
static const platform = const MethodChannel('app.channel.shared.data');
String dataShared = "No Data";
#override
void initState() {
super.initState();
getSharedText();
}
getSharedText() async {
var sharedData = await platform.invokeMethod("getSharedText");
if (sharedData != null) {
setState(() {
dataShared = sharedData;
});
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home:Scaffold(body: Center(child: Text(dataShared)))
);
}
}

Categories

Resources