Getting Started with Flutter Using Platform Channels
This guide explains how to build a multi-platform search experience using the Kotlin API client and Flutter platform channels. You’ll build a classic search interface with Flutter, backed by the Kotlin API Client.
Prepare your project
To use Algolia search, you need an Algolia account. You can create a new account, or use the following credentials:
- Application ID:
latency
- Search API Key:
1f6fd3a6fb973cb08419fe7d288fa4db
- Index name:
bestbuy
These credentials give access to a preloaded dataset of products appropriate for this guide.
Create a new app project
Start by creating a new app.
In a terminal run: flutter create algoliasearch
Open Android Studio and import your newly created project.
Add project dependencies
Update your Kotlin version in android/build.gradle
:
1
2
3
4
buildscript {
ext.kotlin_version = '1.4.32'
//...
}
Add the Kotlin API Client dependency to gradle.build
under algoliasearch/android/app
:
1
2
3
4
5
dependencies {
//...
implementation "com.algolia:algoliasearch-client-kotlin:1.+"
implementation "io.ktor:ktor-client-okhttp:1.+"
}
Replace the library version with the latest available version.
Prepare a platform channel
Flutter uses a flexible system that allows you to call platform-specific Kotlin APIs on Android. Create your platform channel bridging the Kotlin API Client and Flutter search UI.
Create a AlgoliaAPIFlutterAdapter
class that holds the ClientSearch
with your application ID and API key:
1
2
3
4
class AlgoliaAPIFlutterAdapter(applicationID: ApplicationID, apiKey: APIKey) {
val client = ClientSearch(applicationID, apiKey)
}
Add a search
method. This should perform a search request using the provided index name and query parameters, and return the result in the completion using a provided MethodChannel.Result
object.
Since MethodChannel.Result
completion doesn’t support custom types, the ResponseSearch
result must be encoded to a JSON string.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AlgoliaAPIFlutterAdapter(applicationID: ApplicationID, apiKey: APIKey) {
val client = ClientSearch(applicationID, apiKey)
suspend fun search(indexName: IndexName, query: Query, result: MethodChannel.Result) {
val index = client.initIndex(indexName)
try {
val search = index.search(query = query)
result.success(Json.encodeToString(ResponseSearch.serializer(), search))
} catch (e: Exception) {
result.error("AlgoliaNativeError", e.localizedMessage, e.cause)
}
}
}
Next, add the perform
method, which binds the raw Flutter method call to the previously defined search
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class AlgoliaAPIFlutterAdapter(applicationID: ApplicationID, apiKey: APIKey) {
val client = ClientSearch(applicationID, apiKey)
fun perform(call: MethodCall, result: MethodChannel.Result): Unit = runBlocking {
Log.d("AlgoliaAPIAdapter", "method: ${call.method}")
Log.d("AlgoliaAPIAdapter", "args: ${call.arguments}")
val args = call.arguments as? List<String>
if (args == null) {
result.error("AlgoliaNativeError", "Missing arguments", null)
return@runBlocking
}
when (call.method) {
"search" -> search(indexName = args[0].toIndexName(), query = Query(args[1]), result = result)
else -> result.notImplemented()
}
}
suspend fun search(indexName: IndexName, query: Query, result: MethodChannel.Result) {
val index = client.initIndex(indexName)
try {
val search = index.search(query = query)
result.success(Json.encodeToString(ResponseSearch.serializer(), search))
} catch (e: Exception) {
result.error("AlgoliaNativeError", e.localizedMessage, e.cause)
}
}
}
The API adapter is ready.
Add an instance of AlgoliaAPIFlutterAdapter
inside your MainActivity
. The code should look as follows:
1
2
3
4
class MainActivity : FlutterActivity() {
val algoliaAPIAdapter = AlgoliaAPIFlutterAdapter(ApplicationID("latency"), APIKey("1f6fd3a6fb973cb08419fe7d288fa4db"))
}
Override the configureFlutterEngine
method and set the Algolia adapter instance as a method call handler.
1
2
3
4
5
6
7
8
9
10
11
class MainActivity : FlutterActivity() {
val algoliaAPIAdapter = AlgoliaAPIFlutterAdapter(ApplicationID("latency"), APIKey("1f6fd3a6fb973cb08419fe7d288fa4db"))
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.algolia/api").setMethodCallHandler { call, result ->
algoliaAPIAdapter.perform(call, result)
}
}
}
The platform channel is all set, you can access it from the Flutter.
Build a search interface with Flutter
Open ./lib/main.dart
, and add the AlgoliaAPI
class providing a MethodChannel
instance and a convenient search
method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class AlgoliaAPI {
static const platform = const MethodChannel('com.algolia/api');
Future<dynamic> search(String query) async {
try {
var response = await platform.invokeMethod('search', ['instant_search', query]);
return jsonDecode(response);
} on PlatformException catch (_) {
return null;
}
}
}
Then, add a SearchHit
class that represents the search hit. To keep this example simple, it contains a name and an image URL field.
Declare a fromJson
constructor method to conveniently create a SearchHit
from a JSON string.
1
2
3
4
5
6
7
8
9
10
11
12
class SearchHit {
final String name;
final String image;
SearchHit(this.name, this.image);
static SearchHit fromJson(Map<String, dynamic> json) {
return SearchHit(json['name'], json['image']);
}
}
By scrolling down the main.dart
you’ll see a lot of sample code.
Keep it untouched until the _MyHomePageState
class.
Completely remove its sample variables and methods declarations.
Then, add the AlgoliaAPI
object:
1
AlgoliaAPI algoliaAPI = AlgoliaAPI();
Next, add the _searchText
and the _hitsList
properties which will keep the state of your query text and the list of hits respectively.
1
2
String _searchText = "";
List<SearchHit> _hitsList = [];
Then, add the _textFieldController
that controls and listens to the state of the TextField
component you use as the search bar.
1
TextEditingController _textFieldController = TextEditingController();
Add a _getSearchResult
function that calls the Algolia API and extracts the hits
from the search response.
1
2
3
4
5
6
7
8
9
Future<void> _getSearchResult(String query) async {
var response = await algoliaAPI.search(query);
var hitsList = (response['hits'] as List).map((json) {
return SearchHit.fromJson(json);
}).toList();
setState(() {
_hitsList = hitsList;
});
}
Override the build
method containing the user interface declaration.
The interface will be based on the Scaffold
component. Add the AppBar
with “Algolia & Flutter” as its title, and the Column
component as its body:
1
2
3
4
5
6
7
8
9
10
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Algolia & Flutter'),
),
body: Column(
children: <Widget>[
]));
}
The Column
’s body consists of two children: the search bar and the hits list.
Start with adding a search bar.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Algolia & Flutter'),
),
body: Column(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 1),
height: 44,
child: TextField(
controller: _textFieldController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term',
prefixIcon:
Icon(Icons.search, color: Colors.deepPurple),
suffixIcon: _searchText.isNotEmpty
? IconButton(
onPressed: () {
setState(() {
_textFieldController.clear();
});
},
icon: Icon(Icons.clear),
)
: null),
))
]));
}
Next, add the hits list widget as the second child of the Column
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Algolia & Flutter'),
),
body: Column(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 1),
height: 44,
child: TextField(
controller: _textFieldController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term',
prefixIcon:
Icon(Icons.search, color: Colors.deepPurple),
suffixIcon: _searchText.isNotEmpty
? IconButton(
onPressed: () {
setState(() {
_textFieldController.clear();
});
},
icon: Icon(Icons.clear),
)
: null),
)),
Expanded(
child: _hitsList.isEmpty
? Center(child: Text('No results'))
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _hitsList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
padding: EdgeInsets.all(8),
child: Row(children: <Widget>[
Container(
width: 50,
child: Image.network(
'${_hitsList[index].image}')),
SizedBox(width: 10),
Expanded(
child: Text('${_hitsList[index].name}'))
]));
}))
]));
}
Override the initState
method. Add a TextFieldController
listener so it triggers a search request on each keystroke.
You can also trigger an initial empty search from here.
1
2
3
4
5
6
7
8
9
10
11
12
13
@override
void initState() {
super.initState();
_textFieldController.addListener(() {
if (_searchText != _textFieldController.text) {
setState(() {
_searchText = _textFieldController.text;
});
_getSearchResult(_searchText);
}
});
_getSearchResult('');
}
Finally, override the dispose
method to properly remove the TextFieldController
instance.
1
2
3
4
5
@override
void dispose() {
_textFieldController.dispose();
super.dispose();
}
The final version of your _MyHomePageState
class should look as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class _MyHomePageState extends State<MyHomePage> {
AlgoliaAPI algoliaAPI = AlgoliaAPI();
String _searchText = "";
List<SearchHit> _hitsList = [];
TextEditingController _textFieldController = TextEditingController();
Future<void> _getSearchResult(String query) async {
var response = await algoliaAPI.search(query);
var hitsList = (response['hits'] as List).map((json) {
return SearchHit.fromJson(json);
}).toList();
setState(() {
_hitsList = hitsList;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Algolia & Flutter'),
),
body: Column(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 1),
height: 44,
child: TextField(
controller: _textFieldController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term',
prefixIcon:
Icon(Icons.search, color: Colors.deepPurple),
suffixIcon: _searchText.isNotEmpty
? IconButton(
onPressed: () {
setState(() {
_textFieldController.clear();
});
},
icon: Icon(Icons.clear),
)
: null),
)),
Expanded(
child: _hitsList.isEmpty
? Center(child: Text('No results'))
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: _hitsList.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 50,
padding: EdgeInsets.all(8),
child: Row(children: <Widget>[
Container(
width: 50,
child: Image.network(
'${_hitsList[index].image}')),
SizedBox(width: 10),
Expanded(
child: Text('${_hitsList[index].name}'))
]));
}))
]));
}
@override
void initState() {
super.initState();
_textFieldController.addListener(() {
if (_searchText != _textFieldController.text) {
setState(() {
_searchText = _textFieldController.text;
});
_getSearchResult(_searchText);
}
});
_getSearchResult('');
}
@override
void dispose() {
_textFieldController.dispose();
super.dispose();
}
}
Save your changes in the main.dart
file. Build and run your Android application. You should see the basic search interface built with Flutter.
What’s next?
This tutorial gives an example of bridging native search with the Kotlin API client and Flutter using platform channels. Your real application might be way more complex, so you might want to extend this example by adding more search parameters and more API methods.