diff --git a/platform_channels/android/app/build.gradle b/platform_channels/android/app/build.gradle index 16cb37c64..1f85dc1de 100644 --- a/platform_channels/android/app/build.gradle +++ b/platform_channels/android/app/build.gradle @@ -60,4 +60,5 @@ flutter { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.google.code.gson:gson:2.8.6' } diff --git a/platform_channels/android/app/src/main/kotlin/dev/flutter/platform_channels/MainActivity.kt b/platform_channels/android/app/src/main/kotlin/dev/flutter/platform_channels/MainActivity.kt index dd302c820..50ba80527 100644 --- a/platform_channels/android/app/src/main/kotlin/dev/flutter/platform_channels/MainActivity.kt +++ b/platform_channels/android/app/src/main/kotlin/dev/flutter/platform_channels/MainActivity.kt @@ -7,13 +7,13 @@ package dev.flutter.platform_channels import android.content.Context import android.hardware.Sensor import android.hardware.SensorManager +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.common.* import java.io.InputStream +import java.nio.ByteBuffer class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { @@ -50,7 +50,38 @@ class MainActivity : FlutterActivity() { .setMessageHandler { message, reply -> if (message == "getImage") { val inputStream: InputStream = assets.open("eat_new_orleans.jpg") - reply.reply(inputStream.readBytes()); + reply.reply(inputStream.readBytes()) + } + } + + val petList = mutableListOf>() + val gson = Gson() + + // A BasicMessageChannel for sending petList to Dart. + val stringCodecChannel = BasicMessageChannel(flutterEngine.dartExecutor, "stringCodecDemo", StringCodec.INSTANCE) + + // Registers a MessageHandler for BasicMessageChannel to receive pet details to be + // added in petList and send the it back to Dart using stringCodecChannel. + BasicMessageChannel(flutterEngine.dartExecutor, "jsonMessageCodecDemo", JSONMessageCodec.INSTANCE) + .setMessageHandler { message, reply -> + petList.add(0, gson.fromJson(message.toString(), object : TypeToken>() {}.type)) + stringCodecChannel.send(gson.toJson(mapOf("petList" to petList))) + } + + // Registers a MessageHandler for BasicMessageChannel to receive the index of pet + // details to be removed from the petList and send the petList back to Dart using + // stringCodecChannel. If the index is not in the range of petList, we send null + // back to Dart. + BasicMessageChannel(flutterEngine.dartExecutor, "binaryCodecDemo", BinaryCodec.INSTANCE) + .setMessageHandler { message, reply -> + val index = String(message!!.array()).toInt() + if (index >= 0 && index < petList.size) { + petList.removeAt(index) + val replyMessage = "Removed Successfully" + reply.reply(ByteBuffer.allocateDirect(replyMessage.toByteArray().size).put(replyMessage.toByteArray())) + stringCodecChannel.send(gson.toJson(mapOf("petList" to petList))) + } else { + reply.reply(null) } } } diff --git a/platform_channels/lib/main.dart b/platform_channels/lib/main.dart index ffc0b31de..b6bf1624d 100644 --- a/platform_channels/lib/main.dart +++ b/platform_channels/lib/main.dart @@ -3,6 +3,8 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; +import 'package:platform_channels/src/add_pet_details.dart'; +import 'package:platform_channels/src/pet_list_screen.dart'; import 'package:platform_channels/src/event_channel_demo.dart'; import 'package:platform_channels/src/method_channel_demo.dart'; import 'package:platform_channels/src/platform_image_demo.dart'; @@ -19,6 +21,8 @@ class PlatformChannelSample extends StatelessWidget { '/methodChannelDemo': (context) => MethodChannelDemo(), '/eventChannelDemo': (context) => EventChannelDemo(), '/platformImageDemo': (context) => PlatformImageDemo(), + '/petListScreen': (context) => PetListScreen(), + '/addPetDetails': (context) => AddPetDetails(), }, title: 'Platform Channel Sample', home: HomePage(), @@ -47,6 +51,10 @@ List demoList = [ DemoInfo( 'Platform Image Demo', '/platformImageDemo', + ), + DemoInfo( + 'BasicMessageChannel Demo', + '/petListScreen', ) ]; diff --git a/platform_channels/lib/src/add_pet_details.dart b/platform_channels/lib/src/add_pet_details.dart new file mode 100644 index 000000000..ea42908af --- /dev/null +++ b/platform_channels/lib/src/add_pet_details.dart @@ -0,0 +1,86 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:platform_channels/src/pet_list_message_channel.dart'; + +/// Demonstrates how to use [BasicMessageChannel] to send a message to platform. +/// +/// The widget uses [TextField] and [RadioListTile] to take the [PetDetails.breed] and +/// [PetDetails.petType] from the user respectively. +class AddPetDetails extends StatefulWidget { + @override + _AddPetDetailsState createState() => _AddPetDetailsState(); +} + +class _AddPetDetailsState extends State { + final breedTextController = TextEditingController(); + String petType = 'Dog'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Add Pet Details'), + actions: [ + IconButton( + icon: Icon(Icons.add), + onPressed: () { + PetListMessageChannel.addPetDetails( + PetDetails( + petType: petType, + breed: breedTextController.text, + ), + ); + + Navigator.pop(context); + }, + ) + ], + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + SizedBox( + height: 8, + ), + TextField( + controller: breedTextController, + decoration: InputDecoration( + border: OutlineInputBorder(), + filled: true, + hintText: 'Breed of pet', + labelText: 'Breed', + ), + ), + SizedBox( + height: 8, + ), + RadioListTile( + title: const Text('Dog'), + value: 'Dog', + groupValue: petType, + onChanged: (value) { + setState(() { + petType = value; + }); + }, + ), + RadioListTile( + title: const Text('Cat'), + value: 'Cat', + groupValue: petType, + onChanged: (value) { + setState(() { + petType = value; + }); + }, + ), + ], + ), + ), + ); + } +} diff --git a/platform_channels/lib/src/pet_list_message_channel.dart b/platform_channels/lib/src/pet_list_message_channel.dart new file mode 100644 index 000000000..14410eaea --- /dev/null +++ b/platform_channels/lib/src/pet_list_message_channel.dart @@ -0,0 +1,84 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter/services.dart'; + +/// This class includes two methods [addPetDetails] and [removePet] which are used +/// to add a new pet and remove a pet from the the list respectively. +class PetListMessageChannel { + static final _jsonMessageCodecChannel = + BasicMessageChannel('jsonMessageCodecDemo', JSONMessageCodec()); + + static final _binaryCodecChannel = + BasicMessageChannel('binaryCodecDemo', BinaryCodec()); + + /// Method to add a new pet to the list. + /// + /// Demonstrates how to use [BasicMessageChannel] and [JSONMessageCodec] to + /// send more structured data to platform like a [Map] in this case. + static void addPetDetails(PetDetails petDetails) { + _jsonMessageCodecChannel.send(petDetails.toJson()); + } + + /// Method to remove a pet from the list. + /// + /// Demonstrates how to use [BasicMessageChannel] and [BinaryCodec] to + /// send [ByteData] to platform. If the reply received is null, then + /// we will throw a [PlatformException]. + static Future removePet(int index) async { + final uInt8List = utf8.encoder.convert(index.toString()); + final reply = await _binaryCodecChannel.send(uInt8List.buffer.asByteData()); + if (reply == null) { + throw PlatformException( + code: 'INVALID INDEX', + message: 'Failed to delete pet details', + details: null, + ); + } + } +} + +/// A model class that provides [petList] which is received from platform. +class PetListModel { + PetListModel({ + this.petList, + }); + + final List petList; + + /// Method that maps the incoming string of json object to List of [PetDetails]. + factory PetListModel.fromJson(String jsonString) { + final jsonData = json.decode(jsonString) as Map; + return PetListModel( + petList: List.from((jsonData['petList'] as List).map( + (dynamic petDetailsMap) => PetDetails.fromMap( + petDetailsMap as Map, + ), + )), + ); + } +} + +/// A simple model that provides pet details like [petType] and [breed] of pet. +class PetDetails { + PetDetails({ + this.petType, + this.breed, + }); + + final String petType; + final String breed; + + factory PetDetails.fromMap(Map map) => PetDetails( + petType: map['petType'] as String, + breed: map['breed'] as String, + ); + + Map toJson() => { + 'petType': petType, + 'breed': breed, + }; +} diff --git a/platform_channels/lib/src/pet_list_screen.dart b/platform_channels/lib/src/pet_list_screen.dart new file mode 100644 index 000000000..f86fc2cac --- /dev/null +++ b/platform_channels/lib/src/pet_list_screen.dart @@ -0,0 +1,91 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:platform_channels/src/pet_list_message_channel.dart'; + +/// Demonstrates how to use [BasicMessageChannel] to send & receive the platform +/// Message. +class PetListScreen extends StatefulWidget { + @override + _PetListScreenState createState() => _PetListScreenState(); +} + +class _PetListScreenState extends State { + PetListModel petListModel; + + @override + void initState() { + super.initState(); + // Receives a string of json object from the platform and converts it + // to PetModel. + BasicMessageChannel('stringCodecDemo', StringCodec()) + .setMessageHandler((message) async { + setState(() { + petListModel = PetListModel.fromJson(message); + }); + return; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Pet List'), + ), + floatingActionButton: FloatingActionButton( + child: Icon(Icons.add), + onPressed: () { + Navigator.pushNamed(context, '/addPetDetails'); + }, + ), + body: petListModel?.petList?.isEmpty ?? true + ? Center(child: Text('Enter Pet Details')) + : BuildPetList(petListModel.petList), + ); + } +} + +/// Shows list of [PetDetails]. +class BuildPetList extends StatelessWidget { + final List petList; + + BuildPetList(this.petList); + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: EdgeInsets.all(8), + itemCount: petList.length, + itemBuilder: (context, index) { + return ListTile( + title: Text('Pet breed: ${petList[index].breed}'), + subtitle: Text( + 'Pet type: ${petList[index].petType}', + ), + trailing: IconButton( + icon: Icon(Icons.delete), + onPressed: () async { + try { + await PetListMessageChannel.removePet(index); + showSnackBar('Removed successfully!', context); + } catch (error) { + showSnackBar(error.message.toString(), context); + } + }, + ), + ); + }, + ); + } + + void showSnackBar(String message, BuildContext context) { + Scaffold.of(context).showSnackBar(SnackBar( + backgroundColor: Theme.of(context).primaryColor, + content: Text(message), + )); + } +} diff --git a/platform_channels/test/src/add_pet_details_test.dart b/platform_channels/test/src/add_pet_details_test.dart new file mode 100644 index 000000000..6486e6672 --- /dev/null +++ b/platform_channels/test/src/add_pet_details_test.dart @@ -0,0 +1,37 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform_channels/src/add_pet_details.dart'; + +void main() { + group('AddPetDetails tests', () { + var petList = []; + + setUpAll(() { + BasicMessageChannel('jsonMessageCodecDemo', JSONMessageCodec()) + .setMockMessageHandler((dynamic message) async { + petList.add(message as Map); + }); + }); + + testWidgets('Enter pet details', (tester) async { + await tester.pumpWidget(MaterialApp(home: AddPetDetails())); + + // Enter the breed of cat. + await tester.enterText(find.byType(TextField), 'Persian'); + // Select cat from the pet type. + await tester.tap(find.text('Cat')); + + // Initially the list will be empty. + expect(petList, isEmpty); + await tester.tap(find.byIcon(Icons.add)); + + expect(petList, isNotEmpty); + expect(petList.last['breed'], 'Persian'); + }); + }); +} diff --git a/platform_channels/test/src/pet_list_screen_test.dart b/platform_channels/test/src/pet_list_screen_test.dart new file mode 100644 index 000000000..d7967695d --- /dev/null +++ b/platform_channels/test/src/pet_list_screen_test.dart @@ -0,0 +1,83 @@ +// Copyright 2020 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:platform_channels/src/pet_list_message_channel.dart'; +import 'package:platform_channels/src/pet_list_screen.dart'; + +void main() { + group('PetListScreen tests', () { + final basicMessageChannel = + BasicMessageChannel('stringCodecDemo', StringCodec()); + + var petList = [ + { + 'petType': 'Dog', + 'breed': 'Pug', + } + ]; + + PetListModel petListModel; + + setUpAll(() { + // Mock for the pet list received from the platform. + basicMessageChannel.setMockMessageHandler((message) async { + petListModel = PetListModel.fromJson(message); + return; + }); + + // Mock for the index received from the Dart to delete the pet details, + // and send the updated pet list back to Dart. + BasicMessageChannel('binaryCodecDemo', BinaryCodec()) + .setMockMessageHandler((message) async { + // Convert the ByteData to String. + final index = utf8.decoder.convert(message.buffer + .asUint8List(message.offsetInBytes, message.lengthInBytes)); + + // Remove the pet details at the given index. + petList.removeAt(int.parse(index)); + + // Send the updated petList back. + final map = {'petList': petList}; + await basicMessageChannel.send(json.encode(map)); + return; + }); + }); + + test('convert json message to PetListModel', () { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Initially petListModel will be null. + expect(petListModel, isNull); + + // Send the pet list using BasicMessageChannel. + final map = {'petList': petList}; + basicMessageChannel.send(json.encode(map)); + + // Get the details of first pet. + final petDetails = petListModel.petList.first; + expect(petDetails.petType, 'Dog'); + expect(petDetails.breed, 'Pug'); + }); + + testWidgets('BuildPetList test', (tester) async { + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: BuildPetList(petListModel.petList), + ), + )); + + expect(find.text('Pet type: Dog'), findsOneWidget); + expect(find.text('Pet breed: Pug'), findsOneWidget); + + // Delete the pet details. + await tester.tap(find.byIcon(Icons.delete).first); + expect(petListModel.petList, isEmpty); + }); + }); +}