1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 14:58:34 +00:00

restructured the add to app samples (#698)

This commit is contained in:
gaaclarke
2021-02-05 10:20:58 -08:00
committed by GitHub
parent ba8ed34582
commit 323c10558d
239 changed files with 931 additions and 256 deletions

View File

@@ -0,0 +1,48 @@
.DS_Store
.dart_tool/
.packages
.pub/
.idea/
.vagrant/
.sconsign.dblite
.svn/
*.swp
profile
DerivedData/
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
build/
.android/
.ios/
.flutter-plugins
.flutter-plugins-dependencies
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json

View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: c0091b289e9966b6da99647b2f18dcb93de1f207
channel: master
project_type: module

View File

@@ -0,0 +1,50 @@
# Books add-to-app sample
This application simulates a mock scenario in which an existing app with
business logic and middleware already exists. It demonstrates how to add Flutter
to an app that has established patterns for these domains. For more information
on how to use it, see the [README.md](../README.md) parent directory.
This application also utilizes the [Pigeon](https://pub.dev/packages/pigeon)
plugin to avoid manual platform channel wiring. Pigeon autogenerates the
platform channel code in Dart/Java/Objective-C to allow interop using higher
order functions and data classes instead of string-encoded methods and
serialized primitives.
The Pigeon autogenerated code is checked-in and ready to use. If the schema
in `pigeon/schema.dart` is updated, the generated classes can also be re-
generated using:
```shell
flutter pub run pigeon \
--input pigeon/schema.dart \
--java_out ../android_books/app/src/main/java/dev/flutter/example/books/Api.java \
--java_package "dev.flutter.example.books"
```
## Demonstrated concepts
* An existing books catalog app is already implemented in Kotlin and Swift.
* The platform-side app has existing middleware constraints that should also
be the middleware foundation for the additional Flutter screen.
* On Android, the Kotlin app already uses GSON and OkHttp for networking and
references the Google Books API as a data source. These same libraries
also underpin the data fetched and shown in the Flutter screen.
* The platform application interfaces with the Flutter book details page using
idiomatic platform API conventions rather than Flutter conventions.
* On Android, the Flutter activity receives the book to show via activity
intent and returns the edited book by setting the result intent on the
activity. No Flutter concepts are leaked into the consumer activity.
* The [pigeon](https://pub.dev/packages/pigeon) plugin is used to generate
interop APIs and data classes. The same `Book` model class is used within the
Kotlin/Swift program, the Dart program and in the interop between Kotlin/Swift
and Dart.
## More info
For more information about Flutter, check out
[flutter.dev](https://flutter.dev).
For instructions on how to integrate Flutter modules into your existing
applications, see Flutter's
[add-to-app documentation](https://flutter.dev/docs/development/add-to-app).

View File

@@ -0,0 +1,100 @@
// Autogenerated from Pigeon (v0.1.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import
import 'dart:async';
import 'package:flutter/services.dart';
class Book {
String title;
String subtitle;
String author;
String description;
String publishDate;
int pageCount;
// ignore: unused_element
Map<dynamic, dynamic> _toMap() {
final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{};
pigeonMap['title'] = title;
pigeonMap['subtitle'] = subtitle;
pigeonMap['author'] = author;
pigeonMap['description'] = description;
pigeonMap['publishDate'] = publishDate;
pigeonMap['pageCount'] = pageCount;
return pigeonMap;
}
// ignore: unused_element
static Book _fromMap(Map<dynamic, dynamic> pigeonMap) {
final Book result = Book();
result.title = pigeonMap['title'];
result.subtitle = pigeonMap['subtitle'];
result.author = pigeonMap['author'];
result.description = pigeonMap['description'];
result.publishDate = pigeonMap['publishDate'];
result.pageCount = pigeonMap['pageCount'];
return result;
}
}
abstract class FlutterBookApi {
void displayBookDetails(Book arg);
static void setup(FlutterBookApi api) {
{
const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
'dev.flutter.pigeon.FlutterBookApi.displayBookDetails',
StandardMessageCodec());
channel.setMessageHandler((dynamic message) async {
final Map<dynamic, dynamic> mapMessage =
message as Map<dynamic, dynamic>;
final Book input = Book._fromMap(mapMessage);
api.displayBookDetails(input);
});
}
}
}
class HostBookApi {
Future<void> cancel() async {
const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
'dev.flutter.pigeon.HostBookApi.cancel', StandardMessageCodec());
final Map<dynamic, dynamic> replyMap = await channel.send(null);
if (replyMap == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
details: null);
} else if (replyMap['error'] != null) {
final Map<dynamic, dynamic> error = replyMap['error'];
throw PlatformException(
code: error['code'],
message: error['message'],
details: error['details']);
} else {
// noop
}
}
Future<void> finishEditingBook(Book arg) async {
final Map<dynamic, dynamic> requestMap = arg._toMap();
const BasicMessageChannel<dynamic> channel = BasicMessageChannel<dynamic>(
'dev.flutter.pigeon.HostBookApi.finishEditingBook',
StandardMessageCodec());
final Map<dynamic, dynamic> replyMap = await channel.send(requestMap);
if (replyMap == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
details: null);
} else if (replyMap['error'] != null) {
final Map<dynamic, dynamic> error = replyMap['error'];
throw PlatformException(
code: error['code'],
message: error['message'],
details: error['details']);
} else {
// noop
}
}
}

View File

@@ -0,0 +1,201 @@
// 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_module_books/api.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: const Color(0xff6200ee),
),
home: BookDetail(),
);
}
}
typedef BookReceived = void Function(Book book);
class FlutterBookApiHandler extends FlutterBookApi {
FlutterBookApiHandler(this.callback);
final BookReceived callback;
@override
void displayBookDetails(Book book) {
assert(
book != null,
'Non-null book expected from FlutterBookApi.displayBookDetails call.',
);
callback(book);
}
}
class BookDetail extends StatefulWidget {
const BookDetail({this.hostApi, this.flutterApi});
// These are the outgoing and incoming APIs that are here for injection for
// tests.
final HostBookApi hostApi;
final FlutterBookApi flutterApi;
@override
_BookDetailState createState() => _BookDetailState();
}
class _BookDetailState extends State<BookDetail> {
Book book;
HostBookApi hostApi;
FocusNode textFocusNode = FocusNode();
TextEditingController titleTextController = TextEditingController();
TextEditingController subtitleTextController = TextEditingController();
TextEditingController authorTextController = TextEditingController();
@override
void initState() {
super.initState();
// This `HostBookApi` class instance lets us make outgoing calls to the
// platform.
hostApi = widget.hostApi ?? HostBookApi();
// Registering this `FlutterBookApiHandler` class lets us receive incoming
// calls from the platform.
// TODO(gaaclarke): make the setup method an instance method so it's
// injectable https://github.com/flutter/flutter/issues/59119.
FlutterBookApi.setup(FlutterBookApiHandler(
// The `FlutterBookApi` just has one method. Just give a closure for that
// method to the handler class.
(Book book) {
setState(() {
// This book model is what we're going to return to Kotlin eventually.
// Keep it bound to the UI.
this.book = book;
titleTextController.text = book.title;
titleTextController.addListener(() {
this.book?.title = titleTextController.text;
});
// Subtitle could be null.
// TODO(gaaclarke): https://github.com/flutter/flutter/issues/59118.
subtitleTextController.text = book.subtitle ?? '';
subtitleTextController.addListener(() {
this.book?.subtitle = subtitleTextController.text;
});
authorTextController.text = book.author;
authorTextController.addListener(() {
this.book?.author = authorTextController.text;
});
});
}));
}
void clear() {
book = null;
// Keep focus if going to the home screen but unfocus if leaving
// the activity.
textFocusNode.unfocus();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Book Details'),
leading: IconButton(
icon: Icon(Icons.clear),
// Pressing clear cancels the edit and leaves the activity without
// modification.
onPressed: () {
hostApi.cancel();
clear();
},
),
actions: [
IconButton(
icon: Icon(Icons.check),
// Pressing save sends the updated book to the platform.
onPressed: () {
hostApi.finishEditingBook(book);
clear();
},
),
],
),
body: book == null
// Draw a spinner until the platform gives us the book to show details
// for.
? Center(child: CircularProgressIndicator())
: Focus(
focusNode: textFocusNode,
child: ListView(
padding: EdgeInsets.all(24),
children: [
TextField(
controller: titleTextController,
decoration: InputDecoration(
border: OutlineInputBorder(),
filled: true,
hintText: "Title",
labelText: "Title",
),
),
SizedBox(height: 24),
TextField(
controller: subtitleTextController,
maxLines: 2,
decoration: InputDecoration(
border: OutlineInputBorder(),
filled: true,
hintText: "Subtitle",
labelText: "Subtitle",
),
),
SizedBox(height: 24),
TextField(
controller: authorTextController,
decoration: InputDecoration(
border: OutlineInputBorder(),
filled: true,
hintText: "Author",
labelText: "Author",
),
),
SizedBox(height: 32),
Divider(),
Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${book.pageCount} pages ~ published ${book.publishDate}'),
),
),
Divider(),
SizedBox(height: 32),
Center(
child: Text(
'BOOK DESCRIPTION',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
decoration: TextDecoration.underline,
),
),
),
SizedBox(height: 12),
Text(
book.description,
style: TextStyle(color: Colors.grey.shade600, height: 1.24),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,32 @@
// 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:pigeon/pigeon.dart';
class Book {
String title;
String subtitle;
String author;
String description;
String publishDate;
int pageCount;
// Thumbnail thumbnail;
}
// TODO(gaaclarke): add this back when the https://github.com/flutter/flutter/issues/58896
// crash is resolved.
// class Thumbnail {
// String url;
// }
@FlutterApi()
abstract class FlutterBookApi {
void displayBookDetails(Book book);
}
@HostApi()
abstract class HostBookApi {
void cancel();
void finishEditingBook(Book book);
}

View File

@@ -0,0 +1,342 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "12.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.40.6"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.5.0-nullsafety.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
build:
dependency: transitive
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
built_collection:
dependency: transitive
description:
name: built_collection
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.2"
built_value:
dependency: transitive
description:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "7.1.0"
characters:
dependency: transitive
description:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.3"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "3.5.0"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0-nullsafety.3"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.10"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.1"
fixnum:
dependency: transitive
description:
name: fixnum
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.11"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.2"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.4"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.10-nullsafety.1"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
mockito:
dependency: "direct dev"
description:
name: mockito
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.3"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.1"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.2"
pigeon:
dependency: "direct dev"
description:
name: pigeon
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.17"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.10+1"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0-nullsafety.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.10.0-nullsafety.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.19-nullsafety.2"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+15"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.10.0 <2.11.0"

View File

@@ -0,0 +1,33 @@
name: flutter_module_books
description: A Flutter module using the Pigeon package to demonstrate
integrating Flutter in a realistic scenario where the existing platform app
already has business logic and middleware constraints.
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
pigeon: ^0.1.0
mockito: ^4.1.1
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
# This section identifies your Flutter project as a module meant for
# embedding in a native host app. These identifiers should _not_ ordinarily
# be changed after generation - they are used to ensure that the tooling can
# maintain consistency when adding or modifying assets and plugins.
# They also do not have any bearing on your native host application's
# identifiers, which may be completely independent or the same as these.
module:
androidX: true
androidPackage: dev.flutter.example.flutter_module_books
iosBundleIdentifier: dev.flutter.example.flutterModuleBooks

View File

@@ -0,0 +1,43 @@
// 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_module_books/api.dart';
import 'package:flutter_module_books/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
void main() {
testWidgets('Pressing clear calls the cancel API',
(WidgetTester tester) async {
MockHostBookApi mockHostApi = MockHostBookApi();
await tester.pumpWidget(
MaterialApp(
home: BookDetail(hostApi: mockHostApi),
),
);
await tester.tap(find.byIcon(Icons.clear));
verify(mockHostApi.cancel());
});
testWidgets('Pressing done calls the finish editing API',
(WidgetTester tester) async {
MockHostBookApi mockHostApi = MockHostBookApi();
await tester.pumpWidget(
MaterialApp(
home: BookDetail(hostApi: mockHostApi),
),
);
await tester.tap(find.byIcon(Icons.check));
verify(mockHostApi.finishEditingBook(any));
});
}
class MockHostBookApi extends Mock implements HostBookApi {}