Invoke Kotlin Code to Flutter
Introduction
The first thing before start to content is what is the invoke code and why we need to invoke code from native platforms to flutter.Let’s think some scenarios that not used frequently. For example you need a feature that blocks user the call your number or you want to talk only 30 seconds then just end the call. These scenarios not frequently used. Thats the other reason why we need to invokeMethod
. Because we may not find a package for these scenarios.Thats the first reason of using invokeMethod
. Also there are more advantages of using invokeMethod
.
- Platform Integration:
invokeMethod
allows you to access native platform APIs and functionalities that are not available directly in Flutter packages. It enables you to leverage the full power of the underlying platform by bridging the gap between Flutter and the native code. - Customization: If you need to perform complex or custom operations that are not provided by existing Flutter packages, you can create your own platform-specific code and invoke it using
invokeMethod
. This gives you the flexibility to tailor your app's behavior according to specific platform requirements. - Performance Optimization: In certain scenarios, invoking platform-specific code can lead to better performance compared to using Flutter packages. For example, if you need to process large amounts of data or perform computationally intensive tasks, implementing them natively can be more efficient.
- Access to Native UI Components: Flutter provides a rich set of UI components, but there may be cases where you want to use platform-specific UI elements. By using
invokeMethod
, you can integrate native UI components seamlessly into your Flutter app, providing a consistent user experience across platforms. - Compatibility:
invokeMethod
ensures compatibility with future platform updates. As Flutter evolves, new features and APIs are introduced andinvokeMethod
allows you to easily adopt these changes by updating the platform-specific code without affecting the Flutter codebase.
It’s important to note that while invokeMethod
offers these advantages, it also introduces platform-specific dependencies and requires additional development effort. Therefore, it should be used judiciously when there is a genuine need for platform integration or customization that cannot be achieved through existing Flutter packages. I tried to explain why we are using invokeMethod
. So let’s start to our example and see how easy it is.
Fetching Contacts in Kotlin
The first step is creating new flutter project called native_contacts.Then we can open android folder with Android Studio or any IDE that supports Kotlin. I will use Android Studio.
If the android file opened first time it may take 5 minutes to finish configuration. After finish the configuration let’s take a look at our folder structure.
Let’s also take a look MainActivity.kt file. Because we will write code there.
Adding MethodChannel Function
Now we need to add methodChannel function. This function commonly used. So you can copy paste this.
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "example.com/channel").setMethodCallHandler {
call, result ->
if(call.method == "getContacts") {
}
}
You can change “getContacts” string. This name is very important for us becuse on flutter side we need to use same value to use invokeMethod. So let’s go to next step.
Adding Permissions
We are going to fetch user contacts. To do that we need user permission. Let’s add our permissions to AndroidManifest.xml file.
Checking Permissions
We sholud add our permissions to our AndroidManifest.xml file. So let’s add if statement for these permissions.
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_CONTACTS),
0
)
}
This code checks that permission is granted or not. If permission is not granted a permission dialog pops up .
Creating Data Class
Now we can access to contacts. Before we reach to our contacts let’s create a data class called Contact. Our model’s parameters will be id,name and number.
Fetching Contacts
Everything we need is ready to fetch contacts. So let’s create a function that returns a contacts list and add our Contact items one by one.
fun getNamePhoneDetails(): MutableList<Contact> {
val contacts = mutableListOf<Contact>()
val cr = contentResolver
val cur = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
null, null, null)
if (cur!!.count > 0) {
while (cur.moveToNext()) {
val id = cur.getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NAME_RAW_CONTACT_ID))
val name = cur.getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val number = cur.getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
contacts.add(Contact(id,name,number))
} } return contacts
}
Send Contacts to Flutter
Now we have the contacts list. Let’s send it to flutter side.
var contacts = getNamePhoneDetails();
println(contacts)
/// distincBy filter the list and not let to add same contact again
/// also convert contacts to map to catch easily in flutter as a
val contactMaps = contacts.distinctBy { it.name }.map { it.toMap() } result.success(contactMaps)
We have some trick here. We used distinctBy method. This method provides us to add contact value just once. Also we convert our list to map. Because if we send a “contacts” list, flutter will throw this “Unsupported value” error.
Failed to handle method call
E/MethodChannel#example.com/channel(26720): java.lang.IllegalArgumentException: Unsupported value
Thats the reason we converted data to map.
Full Kotlin Code
package com.example.native_contacts
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.provider.ContactsContract
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannelclass MainActivity: FlutterActivity() { @SuppressLint("Range", "SuspiciousIndentation")
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "example.com/channel").setMethodCallHandler {
call, result ->
if(call.method == "getContacts") {
/// Check Contact Permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_CONTACTS),
0
)
} else { var contacts = getNamePhoneDetails();
println(contacts) /// distincBy filter the list and not let to add same contact again
/// also convert contacts to map to catch easily in flutter as a
val contactMaps = contacts.distinctBy { it.name }.map { it.toMap() } result.success(contactMaps)
} }
else {
result.notImplemented()
}
}
}
@SuppressLint("Range")
fun getNamePhoneDetails(): MutableList<Contact> { val contacts = mutableListOf<Contact>()
val cr = contentResolver
val cur = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
null, null, null)
if (cur!!.count > 0) {
while (cur.moveToNext()) {
val id = cur.getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NAME_RAW_CONTACT_ID))
val name = cur.getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
val number = cur.getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
contacts.add(Contact(id,name,number))
} } return contacts
}
}data class Contact(val id: String, val name: String, val number: String) {
fun toMap(): Map<String, Any> {
return mapOf(
"id" to id,
"name" to name,
"number" to number
)
}}
Let’s jump the flutter side, create contact model and try to fetch our contact list with invokeMethod.
Creating ContactModel
We can easily create our dart model file with this website.
Let’s create new page called contacts_page.dart
// initial methodCannel variable
static const platform = MethodChannel('example.com/channel');
The first thing we are going to do is create method channel variable. Then let’s fetch our list.
Invoke Method
Let’s create a new function called fetchContacts. We will call this function on initState.I try to explain this function with comments but also I will also explain it here. Our data variable return us “Object” value and not accepting the cast methods. But we need to convert our objects to ourContactsModel type. To do this firstly we should get our data. Then seperate all items in contacts list with for (var item in data) method. In loop we create new ContactsModel value and fill it with our current contact item. After for loop we just need to set our contacts list. Also you are going to see sort method. You may add or use it if you want to. It is not necessary.
// fetch contacts from native kotlin
Future<void> fetchContacts() async {
try {
// call invoke method
// method name must be same
var data = await platform.invokeMethod('getContacts');
List<ContactsModel> contacts = []; /// there is one little trick here
/// we are getting conctact from kotlin as Contact model type
/// but invoke method returns Object;
/// to conver our Objects to [ContactsModel]
/// we have to seperate objects and
/// get their id,name,number one by one.
/// Then we just need to add contact to [contacts] list. for (var item in data) {
ContactsModel contact = ContactsModel(
id: item['id'], name: item['name'], number: item['number']); contacts.add(contact); /// its sorting operation depends on
/// contact id.It is not necessary but
/// it seems better on ui (My think)
contacts.sort((a, b) {
int firstId = int.parse(a.id.toString());
int secondId = int.parse(b.id.toString()); return firstId.compareTo(secondId);
});
} /// After getting contacts one by one
/// we just need to set [reveivedContacts]
setState(() {
reveivedContacts = contacts;
});
} on PlatformException catch (e) {
debugPrint(e.message);
}
}
UI Part
Now we have contact list in flutter by using invokeMethod. So let’s display our values in the list.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Contacts"),
),
body: ListView.builder(
itemCount: reveivedContacts.length,
itemBuilder: (context, index) {
ContactsModel contact = reveivedContacts[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.orange,
child: Text(contact.id.toString(),
style: const TextStyle(color: Colors.white)),
),
title: Text(
reveivedContacts[index].name.toString(),
),
subtitle: Text(reveivedContacts[index].number ?? ''),
);
})
// This trailing comma makes auto-formatting nicer for build methods.
);
}
Full Contacts Page Code
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:native_contacts/models/contact_models.dart';
class ContactsPage extends StatefulWidget {
const ContactsPage({
super.key,
}); @override
State<ContactsPage> createState() => _ContactsPageState();
}class _ContactsPageState extends State<ContactsPage> {
// initial methodCannel variable
static const platform = MethodChannel('example.com/channel'); // initial contact list
List<ContactsModel> reveivedContacts = []; // fetch contacts from native kotlin
Future<void> fetchContacts() async {
try {
// call invoke method
// method name must be same
var data = await platform.invokeMethod('getContacts'); List<ContactsModel> contacts = []; /// there is one little trick here
/// we are getting conctact from kotlin as Contact model type
/// but invoke method returns Object;
/// to conver our Objects to [ContactsModel]
/// we have to seperate objects and
/// get their id,name,number one by one.
/// Then we just need to add contact to [contacts] list. for (var item in data) {
ContactsModel contact = ContactsModel(
id: item['id'], name: item['name'], number: item['number']); contacts.add(contact); /// its sorting operation depends on
/// contact id.It is not necessary but
/// it seems better on ui (My think)
contacts.sort((a, b) {
int firstId = int.parse(a.id.toString());
int secondId = int.parse(b.id.toString()); return firstId.compareTo(secondId);
});
} /// After getting contacts one by one
/// we just need to set [reveivedContacts]
setState(() {
reveivedContacts = contacts;
});
} on PlatformException catch (e) {
debugPrint(e.message);
}
} @override
void initState() {
fetchContacts();
super.initState();
} @override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Contacts"),
),
body: ListView.builder(
itemCount: reveivedContacts.length,
itemBuilder: (context, index) {
ContactsModel contact = reveivedContacts[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.orange,
child: Text(contact.id.toString(),
style: const TextStyle(color: Colors.white)),
),
title: Text(
reveivedContacts[index].name.toString(),
),
subtitle: Text(reveivedContacts[index].number ?? ''),
);
})
// This trailing comma makes auto-formatting nicer for build methods.
);
}
}
This content may show steps harder than it is but if you see all project and code you will also see how easy it is.
Good luck.