ANDROID + FLUTTER TUTORIAL

Use Flutter Screens in Native Android App & Share Data among them

When two parallel worlds follow the same destiny!

Saurabh Pant
Dev Genius
Published in
6 min readMay 25, 2023

--

Recently, we ran into a situation where we’re planning to develop all the platforms on Flutter but our Android App is a native one. There are redesigning of some screens which are being also used on our web portals (which are also in flutter). We realised that wouldn’t it be better if we can create the new designs in Flutter and use them on all portals and apps.

So we came to a question, can we integrate Flutter into Native Android App? And guess what this is possible. Hence we decided to do our new screens in Flutter & will run them as part of our native app.

In this article, I’ll walk you through how can you do such integration and share data from Native code to Flutter code.

Create Flutter Module

To add Flutter into a Native app, we first have to create a new Flutter Module. If you’re using Android Studio latest release then you can follow the steps.

Install the Flutter plugins in Android Studio from plugins option in Preferences

Then go to either File -> New -> New Flutter Project

or from the outside panel as

Keep the Flutter selected and click Next

Fill all the details and make sure to select the Project type as Module and the Project location should be same folder within which your existing android app resides i.e both the existing app and flutter module should be siblings folder.

And the Flutter module is created.

Binding Flutter Module to Native Code

Now we’ll go to our existing Android project and open the app level build.gradle and under the android section, add the following

android {
//...
defaultConfig {
ndk {
// Filter for architectures supported by Flutter.
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
}
}
}

We’ve to make our Flutter module accessible to Android app and then only they’ll be able to work together. To make that happen, we need to provide the Flutter module dependency to our Android app code by append the following code in our settings.gradle file.

setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'/mm_flutter_module/.android/include_flutter.groovy'
))

This sets the module as a sub project for the host Android project. We can then add this module as a dependency in our app level build.gradle as:

implementation project(':flutter') // don't change the name 'flutter'

Sync project and everything should be normal as always 😄. If you’ll be treating the Flutter screens as an activity then we need make an addition to our AndroidManifest as well.

<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" />

Understanding Flutter Engine

This is important to understand that in Flutter, we’ve widgets but no activity/fragments and we’ve route but no intent for navigation. But we still use these Flutter screens as either Activity or Fragment or View and to run them in such a fashion, we would require Flutter Engine. With the help of Flutter Engine, we can select which route we want to navigate to and how to open it in Native app i.e. like either an activity or a fragment. We can either use cached version of Flutter Engine (if we’ve a defined initial route) or can use a new instance of Flutter Engine to define custom route navigations.

DashboardFragment in Native

Let’s say we would want to show a dashboard fragment within our activity. But this dashboard screen is build in our Flutter module. And let’s also assume that we’ve multiple routes in our Flutter module for different screens but we want to open the dashboard route in our DashboardFragment.

Let’s create a DashboardFragment by extending FlutterFragment as

class DashboardFragment: FlutterFragment() {}

And then override the two functions as

override fun provideFlutterEngine(context: Context): FlutterEngine? {
val engine = FlutterEngine(requireActivity())
return engine
}

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)

}

The provideFlutterEngine gives us an instance of FlutterEngine that we can use to set up our custom route and the configureFlutterEngine provides us the ability to communicate with the Flutter module for any data requirement.

Let’s modify our DashboardFragment a bit so that we can access it as a normal fragment.

const val KeyData = "key_data"

class DashboardFragment: FlutterFragment() {

var data: String? = null

companion object {
fun instance(data: String): DashboardFragment {
val fragment = DashboardFragment()
val bundle = Bundle().apply {
putString(KeyData,data)
}
fragment.arguments = bundle
return fragment
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {args ->
if (args.containsKey(KeyData)) {
data = args.getString(KeyData)
}
}
}
...
// other two overriding functions
}

As we can see this looks like a normal fragment we’re used to of. We’ve simply passed a data that we want to pass to our Dashboard screen in Flutter.

Sending data from DashboardFragment

Let’s say our route in Flutter module for dashboard screen is

private val dashboardRoute = "/loan_dashboard"

So we set our dashboard fragment for above route navigation via our FlutterEngine as

override fun provideFlutterEngine(context: Context): FlutterEngine? {
val engine = FlutterEngine(requireActivity())
engine.navigationChannel.setInitialRoute(dashboardRoute)
return engine
}

We then want to set up a mechanism via which we would want our Flutter Dashboard screen to get the data we passed from the native side and we can do it by using MethodChannel as

const val CommunicationChannel = "com.app/dataShare"

class DashboardFragment: FlutterFragment() {

// existing code...
var data: String? = null
private val callbackMethod = "shareData"

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CommunicationChannel).setMethodCallHandler {
call, result ->
if (call.method == callbackMethod) {
data?.let {
result.success(it)
} ?: result.error("error")
} else {
result.notImplemented()
}
}
}
}

Here, we set up a channel which we named as com.app/dataShare. There is also a callbackMethod which is a kind of a code that indicates what we want share when this code is requested from the Flutter module and in our case we pass the data via result. success call.

We need to make sure to keep this channel name and callbackMethod exactly same in the Flutter module too.

And our complete DashboardFragment looks like as

Receiving data on Flutter Side

Assuming we’ve created our Dashboard screen widget already something like

class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});

@override
State<StatefulWidget> createState() => _DashboardScreen();
}

And in our main.dart file we’ve set the dashboard route as

final dashboardRoute = "/loan_dashboard";

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
dashboardRoute: (context) => const DashboardScreen(),
},
initialRoute: dashboardRoute,
);
}

Now we want to receive the data that was passed from the native code and want to do some operation on that data. So like we set a channel for communication from native side, we’ll do similar in Flutter module too.

const communicationChannel = "com.app/dataShare";
const callbackMethod = "shareData";

loadSharedData() async {
var methodChannel = const MethodChannel(communicationChannel);
try {
final data = await methodChannel.invokeMethod(callbackMethod);
if(data != null) {
// do something with the received data from native side
}
} on PlatformException catch (e) {
print('error = ${e.message}');
}
}

In this function above, we’re setting up the channel and trying to invoke a callback method that could be defined on the native side and we wait for the data to be received. This is how we can process the data from native side.

Now we call this function from our Widget init function to call it only once

@override
void initState() {
super.initState();
loadSharedData()
}

And the integration and data sharing completes here!

Bamn! We’ve successfully integrated Flutter screen as a Fragment within our Native app, communicated and passed data from our Native code to Flutter module.

Hope it would help you!

That is all for now! Stay tuned!

Connect with me (if the content is helpful to you ) on

Until next time…

Cheers!

--

--

App Developer (Native & Flutter) | Mentor | Writer | Youtuber @_zaqua | Traveller | Content Author & Course Instructor @Droidcon | Frontend Lead @MahilaMoney