snapshot of https://github.com/flutter/samples.git (main, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Main Branch CI / Test Flutter beta on ubuntu-latest (push) Has been skipped
Main Branch CI / Test Flutter stable on ubuntu-latest (push) Has been skipped
Main Branch CI / Test Flutter beta on macos-latest (push) Has been cancelled
Main Branch CI / Test Flutter stable on macos-latest (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 01:43:26 +05:00
commit 9ed6a80afa
2886 changed files with 140184 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
// 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'news_tab.dart';
import 'profile_tab.dart';
import 'settings_tab.dart';
import 'songs_tab.dart';
import 'widgets.dart';
void main() => runApp(const MyAdaptingApp());
class MyAdaptingApp extends StatelessWidget {
const MyAdaptingApp({super.key});
@override
Widget build(context) {
// Either Material or Cupertino widgets work in either Material or Cupertino
// Apps.
return MaterialApp(
title: 'Adaptive Music App',
theme: ThemeData(
// Use the green theme for Material widgets.
primarySwatch: Colors.green,
),
darkTheme: ThemeData.dark(),
builder: (context, child) {
return CupertinoTheme(
// Instead of letting Cupertino widgets auto-adapt to the Material
// theme (which is green), this app will use a different theme
// for Cupertino (which is blue by default).
data: const CupertinoThemeData(),
child: Material(child: child),
);
},
home: const PlatformAdaptingHomePage(),
);
}
}
// Shows a different type of scaffold depending on the platform.
//
// This file has the most amount of non-sharable code since it behaves the most
// differently between the platforms.
//
// These differences are also subjective and have more than one 'right' answer
// depending on the app and content.
class PlatformAdaptingHomePage extends StatefulWidget {
const PlatformAdaptingHomePage({super.key});
@override
State<PlatformAdaptingHomePage> createState() =>
_PlatformAdaptingHomePageState();
}
class _PlatformAdaptingHomePageState extends State<PlatformAdaptingHomePage> {
// This app keeps a global key for the songs tab because it owns a bunch of
// data. Since changing platform re-parents those tabs into different
// scaffolds, keeping a global key to it lets this app keep that tab's data as
// the platform toggles.
//
// This isn't needed for apps that doesn't toggle platforms while running.
final songsTabKey = GlobalKey();
// In Material, this app uses the hamburger menu paradigm and flatly lists
// all 4 possible tabs. This drawer is injected into the songs tab which is
// actually building the scaffold around the drawer.
Widget _buildAndroidHomePage(BuildContext context) {
return SongsTab(key: songsTabKey, androidDrawer: _AndroidDrawer());
}
// On iOS, the app uses a bottom tab paradigm. Here, each tab view sits inside
// a tab in the tab scaffold. The tab scaffold also positions the tab bar
// in a row at the bottom.
//
// An important thing to note is that while a Material Drawer can display a
// large number of items, a tab bar cannot. To illustrate one way of adjusting
// for this, the app folds its fourth tab (the settings page) into the
// third tab. This is a common pattern on iOS.
Widget _buildIosHomePage(BuildContext context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: const [
BottomNavigationBarItem(
label: SongsTab.title,
icon: SongsTab.iosIcon,
),
BottomNavigationBarItem(
label: NewsTab.title,
icon: NewsTab.iosIcon,
),
BottomNavigationBarItem(
label: ProfileTab.title,
icon: ProfileTab.iosIcon,
),
],
),
tabBuilder: (context, index) {
assert(index <= 2 && index >= 0, 'Unexpected tab index: $index');
return switch (index) {
0 => CupertinoTabView(
defaultTitle: SongsTab.title,
builder: (context) => SongsTab(key: songsTabKey),
),
1 => CupertinoTabView(
defaultTitle: NewsTab.title,
builder: (context) => const NewsTab(),
),
2 => CupertinoTabView(
defaultTitle: ProfileTab.title,
builder: (context) => const ProfileTab(),
),
_ => const SizedBox.shrink(),
};
},
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroidHomePage,
iosBuilder: _buildIosHomePage,
);
}
}
class _AndroidDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DrawerHeader(
decoration: const BoxDecoration(color: Colors.green),
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Icon(
Icons.account_circle,
color: Colors.green.shade800,
size: 96,
),
),
),
ListTile(
leading: SongsTab.androidIcon,
title: const Text(SongsTab.title),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: NewsTab.androidIcon,
title: const Text(NewsTab.title),
onTap: () {
Navigator.pop(context);
Navigator.push<void>(
context,
MaterialPageRoute(builder: (context) => const NewsTab()),
);
},
),
ListTile(
leading: ProfileTab.androidIcon,
title: const Text(ProfileTab.title),
onTap: () {
Navigator.pop(context);
Navigator.push<void>(
context,
MaterialPageRoute(
builder: (context) => const ProfileTab(),
),
);
},
),
// Long drawer contents are often segmented.
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Divider(),
),
ListTile(
leading: SettingsTab.androidIcon,
title: const Text(SettingsTab.title),
onTap: () {
Navigator.pop(context);
Navigator.push<void>(
context,
MaterialPageRoute(
builder: (context) => const SettingsTab(),
),
);
},
),
],
),
);
}
}

View File

@@ -0,0 +1,127 @@
// 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/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_lorem/flutter_lorem.dart';
import 'utils.dart';
import 'widgets.dart';
class NewsTab extends StatefulWidget {
static const title = 'News';
static const androidIcon = Icon(Icons.library_books);
static const iosIcon = Icon(CupertinoIcons.news);
const NewsTab({super.key});
@override
State<NewsTab> createState() => _NewsTabState();
}
class _NewsTabState extends State<NewsTab> {
static const _itemsLength = 20;
late final List<Color> colors;
late final List<String> titles;
late final List<String> contents;
@override
void initState() {
colors = getRandomColors(_itemsLength);
titles = List.generate(
_itemsLength,
(index) => generateRandomHeadline(),
);
contents = List.generate(
_itemsLength,
(index) => lorem(paragraphs: 1, words: 24),
);
super.initState();
}
Widget _listBuilder(BuildContext context, int index) {
return SafeArea(
top: false,
bottom: false,
child: Card(
elevation: 1.5,
margin: const EdgeInsets.fromLTRB(6, 12, 6, 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
child: InkWell(
// Make it splash on Android. It would happen automatically if this
// was a real card but this is just a demo. Skip the splash on iOS.
onTap: defaultTargetPlatform == TargetPlatform.iOS ? null : () {},
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
backgroundColor: colors[index],
child: Text(
titles[index].substring(0, 1),
style: const TextStyle(color: Colors.white),
),
),
const Padding(padding: EdgeInsets.only(left: 16)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
titles[index],
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
const Padding(padding: EdgeInsets.only(top: 8)),
Text(contents[index]),
],
),
),
],
),
),
),
),
);
}
// ===========================================================================
// Non-shared code below because this tab uses different scaffolds.
// ===========================================================================
Widget _buildAndroid(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text(NewsTab.title)),
body: ListView.builder(
itemCount: _itemsLength,
itemBuilder: _listBuilder,
),
);
}
Widget _buildIos(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(),
child: ListView.builder(
itemCount: _itemsLength,
itemBuilder: _listBuilder,
),
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

View File

@@ -0,0 +1,254 @@
// 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'settings_tab.dart';
import 'widgets.dart';
class ProfileTab extends StatelessWidget {
static const title = 'Profile';
static const androidIcon = Icon(Icons.person);
static const iosIcon = Icon(CupertinoIcons.profile_circled);
const ProfileTab({super.key});
Widget _buildBody(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
const Padding(
padding: EdgeInsets.all(8),
child: Center(
child: Text(
'😼',
style: TextStyle(
fontSize: 80,
decoration: TextDecoration.none,
),
),
),
),
const PreferenceCard(
header: 'MY INTENSITY PREFERENCE',
content: '🔥',
preferenceChoices: [
'Super heavy',
'Dial it to 11',
"Head bangin'",
'1000W',
'My neighbor hates me',
],
),
const PreferenceCard(
header: 'CURRENT MOOD',
content: '🤘🏾🚀',
preferenceChoices: [
'Over the moon',
'Basking in sunlight',
'Hello fellow Martians',
'Into the darkness',
],
),
Expanded(child: Container()),
const LogOutButton(),
],
),
),
);
}
// ===========================================================================
// Non-shared code below because on iOS, the settings tab is nested inside of
// the profile tab as a button in the nav bar.
// ===========================================================================
Widget _buildAndroid(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text(title)),
body: _buildBody(context),
);
}
Widget _buildIos(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
trailing: CupertinoButton(
padding: EdgeInsets.zero,
child: SettingsTab.iosIcon,
onPressed: () {
// This pushes the settings page as a full page modal dialog on top
// of the tab bar and everything.
Navigator.of(context, rootNavigator: true).push<void>(
CupertinoPageRoute(
title: SettingsTab.title,
fullscreenDialog: true,
builder: (context) => const SettingsTab(),
),
);
},
),
),
child: _buildBody(context),
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}
class PreferenceCard extends StatelessWidget {
const PreferenceCard({
required this.header,
required this.content,
required this.preferenceChoices,
super.key,
});
final String header;
final String content;
final List<String> preferenceChoices;
@override
Widget build(context) {
return PressableCard(
color: Colors.green,
flattenAnimation: const AlwaysStoppedAnimation(0),
child: Stack(
children: [
SizedBox(
height: 120,
width: 250,
child: Padding(
padding: const EdgeInsets.only(top: 40),
child: Center(
child: Text(content, style: const TextStyle(fontSize: 48)),
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
color: Colors.black12,
height: 40,
padding: const EdgeInsets.only(left: 12),
alignment: Alignment.centerLeft,
child: Text(
header,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
onPressed: () {
showChoices(context, preferenceChoices);
},
);
}
}
class LogOutButton extends StatelessWidget {
static const _logoutMessage = Text(
"You can't actually log out! This is just a demo of how alerts work.",
);
const LogOutButton({super.key});
// ===========================================================================
// Non-shared code below because this tab shows different interfaces. On
// Android, it's showing an alert dialog with 2 buttons and on iOS,
// it's showing an action sheet with 3 choices.
//
// This is a design choice and you may want to do something different in your
// app.
// ===========================================================================
Widget _buildAndroid(BuildContext context) {
return ElevatedButton(
child: const Text('LOG OUT', style: TextStyle(color: Colors.red)),
onPressed: () {
// You should do something with the result of the dialog prompt in a
// real app but this is just a demo.
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Log out?'),
content: _logoutMessage,
actions: [
TextButton(
child: const Text('Got it'),
onPressed: () => Navigator.pop(context),
),
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.pop(context),
),
],
);
},
);
},
);
}
Widget _buildIos(BuildContext context) {
return CupertinoButton(
color: CupertinoColors.destructiveRed,
child: const Text('Log out'),
onPressed: () {
// You should do something with the result of the action sheet prompt
// in a real app but this is just a demo.
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
return CupertinoActionSheet(
title: const Text('Log out?'),
message: _logoutMessage,
actions: [
CupertinoActionSheetAction(
isDestructiveAction: true,
onPressed: () => Navigator.pop(context),
child: const Text('Reprogram the night man'),
),
CupertinoActionSheetAction(
child: const Text('Got it'),
onPressed: () => Navigator.pop(context),
),
],
cancelButton: CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
);
},
);
},
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

View File

@@ -0,0 +1,113 @@
// 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'widgets.dart';
class SettingsTab extends StatefulWidget {
static const title = 'Settings';
static const androidIcon = Icon(Icons.settings);
static const iosIcon = Icon(CupertinoIcons.gear);
const SettingsTab({super.key});
@override
State<SettingsTab> createState() => _SettingsTabState();
}
class _SettingsTabState extends State<SettingsTab> {
var switch1 = false;
var switch2 = true;
var switch3 = true;
var switch4 = true;
var switch5 = true;
var switch6 = false;
var switch7 = true;
Widget _buildList() {
return ListView(
children: [
const Padding(padding: EdgeInsets.only(top: 24)),
ListTile(
title: const Text('Send me marketing emails'),
// The Material switch has a platform adaptive constructor.
trailing: Switch.adaptive(
value: switch1,
onChanged: (value) => setState(() => switch1 = value),
),
),
ListTile(
title: const Text('Enable notifications'),
trailing: Switch.adaptive(
value: switch2,
onChanged: (value) => setState(() => switch2 = value),
),
),
ListTile(
title: const Text('Remind me to rate this app'),
trailing: Switch.adaptive(
value: switch3,
onChanged: (value) => setState(() => switch3 = value),
),
),
ListTile(
title: const Text('Background song refresh'),
trailing: Switch.adaptive(
value: switch4,
onChanged: (value) => setState(() => switch4 = value),
),
),
ListTile(
title: const Text('Recommend me songs based on my location'),
trailing: Switch.adaptive(
value: switch5,
onChanged: (value) => setState(() => switch5 = value),
),
),
ListTile(
title: const Text('Auto-transition playback to cast devices'),
trailing: Switch.adaptive(
value: switch6,
onChanged: (value) => setState(() => switch6 = value),
),
),
ListTile(
title: const Text('Find friends from my contact list'),
trailing: Switch.adaptive(
value: switch7,
onChanged: (value) => setState(() => switch7 = value),
),
),
],
);
}
// ===========================================================================
// Non-shared code below because this tab uses different scaffolds.
// ===========================================================================
Widget _buildAndroid(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text(SettingsTab.title)),
body: _buildList(),
);
}
Widget _buildIos(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(),
child: _buildList(),
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

View File

@@ -0,0 +1,112 @@
// 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/cupertino.dart';
import 'package:flutter/material.dart';
import 'widgets.dart';
/// Page shown when a card in the songs tab is tapped.
///
/// On Android, this page sits at the top of your app. On iOS, this page is on
/// top of the songs tab's content but is below the tab bar itself.
class SongDetailTab extends StatelessWidget {
const SongDetailTab({
required this.id,
required this.song,
required this.color,
super.key,
});
final int id;
final String song;
final Color color;
Widget _buildBody() {
return SafeArea(
bottom: false,
left: false,
right: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Hero(
tag: id,
child: HeroAnimatingSongCard(
song: song,
color: color,
heroAnimation: const AlwaysStoppedAnimation(1),
),
// This app uses a flightShuttleBuilder to specify the exact widget
// to build while the hero transition is mid-flight.
//
// It could either be specified here or in SongsTab.
flightShuttleBuilder:
(
context,
animation,
flightDirection,
fromHeroContext,
toHeroContext,
) {
return HeroAnimatingSongCard(
song: song,
color: color,
heroAnimation: animation,
);
},
),
const Divider(height: 0, color: Colors.grey),
Expanded(
child: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => switch (index) {
0 => const Padding(
padding: EdgeInsets.only(left: 15, top: 16, bottom: 8),
child: Text(
'You might also like:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
_ => const SongPlaceholderTile(),
},
),
),
],
),
);
}
// ===========================================================================
// Non-shared code below because we're using different scaffolds.
// ===========================================================================
Widget _buildAndroid(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(song)),
body: _buildBody(),
);
}
Widget _buildIos(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(song),
previousPageTitle: 'Songs',
),
child: _buildBody(),
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

View File

@@ -0,0 +1,173 @@
// 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/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'song_detail_tab.dart';
import 'utils.dart';
import 'widgets.dart';
class SongsTab extends StatefulWidget {
static const title = 'Songs';
static const androidIcon = Icon(Icons.music_note);
static const iosIcon = Icon(CupertinoIcons.music_note);
const SongsTab({super.key, this.androidDrawer});
final Widget? androidDrawer;
@override
State<SongsTab> createState() => _SongsTabState();
}
class _SongsTabState extends State<SongsTab> {
static const _itemsLength = 50;
final _androidRefreshKey = GlobalKey<RefreshIndicatorState>();
late List<MaterialColor> colors;
late List<String> songNames;
@override
void initState() {
_setData();
super.initState();
}
void _setData() {
colors = getRandomColors(_itemsLength);
songNames = getRandomNames(_itemsLength);
}
Future<void> _refreshData() {
return Future.delayed(
// This is just an arbitrary delay that simulates some network activity.
const Duration(seconds: 2),
() => setState(() => _setData()),
);
}
Widget _listBuilder(BuildContext context, int index) {
if (index >= _itemsLength) return Container();
// Show a slightly different color palette. Show poppy-ier colors on iOS
// due to lighter contrasting bars and tone it down on Android.
final color = defaultTargetPlatform == TargetPlatform.iOS
? colors[index]
: colors[index].shade400;
return SafeArea(
top: false,
bottom: false,
child: Hero(
tag: index,
child: HeroAnimatingSongCard(
song: songNames[index],
color: color,
heroAnimation: const AlwaysStoppedAnimation(0),
onPressed: () => Navigator.of(context).push<void>(
MaterialPageRoute(
builder: (context) => SongDetailTab(
id: index,
song: songNames[index],
color: color,
),
),
),
),
),
);
}
void _togglePlatform() {
if (defaultTargetPlatform == TargetPlatform.iOS) {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
} else {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
}
// This rebuilds the application. This should obviously never be
// done in a real app but it's done here since this app
// unrealistically toggles the current platform for demonstration
// purposes.
WidgetsBinding.instance.reassembleApplication();
}
// ===========================================================================
// Non-shared code below because:
// - Android and iOS have different scaffolds
// - There are different items in the app bar / nav bar
// - Android has a hamburger drawer, iOS has bottom tabs
// - The iOS nav bar is scrollable, Android is not
// - Pull-to-refresh works differently, and Android has a button to trigger it too
//
// And these are all design time choices that doesn't have a single 'right'
// answer.
// ===========================================================================
Widget _buildAndroid(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(SongsTab.title),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () async =>
await _androidRefreshKey.currentState!.show(),
),
IconButton(
icon: const Icon(Icons.shuffle),
onPressed: _togglePlatform,
),
],
),
drawer: widget.androidDrawer,
body: RefreshIndicator(
key: _androidRefreshKey,
onRefresh: _refreshData,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 12),
itemCount: _itemsLength,
itemBuilder: _listBuilder,
),
),
);
}
Widget _buildIos(BuildContext context) {
return CustomScrollView(
slivers: [
CupertinoSliverNavigationBar(
trailing: CupertinoButton(
padding: EdgeInsets.zero,
onPressed: _togglePlatform,
child: const Icon(CupertinoIcons.shuffle),
),
),
CupertinoSliverRefreshControl(onRefresh: _refreshData),
SliverSafeArea(
top: false,
sliver: SliverPadding(
padding: const EdgeInsets.symmetric(vertical: 12),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
_listBuilder,
childCount: _itemsLength,
),
),
),
),
],
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

View File

@@ -0,0 +1,76 @@
// 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:math';
import 'package:english_words/english_words.dart';
// ignore: implementation_imports
import 'package:flutter/material.dart';
// This file has a number of platform-agnostic non-Widget utility functions.
const _myListOfRandomColors = [
Colors.red,
Colors.blue,
Colors.teal,
Colors.yellow,
Colors.amber,
Colors.deepOrange,
Colors.green,
Colors.indigo,
Colors.lime,
Colors.pink,
Colors.orange,
];
final _random = Random();
// Avoid customizing the word generator, which can be slow.
// https://github.com/filiph/english_words/issues/9
final wordPairIterator = generateWordPairs();
String generateRandomHeadline() {
final artist = capitalizePair(wordPairIterator.first);
return switch (_random.nextInt(10)) {
0 => '$artist says ${nouns[_random.nextInt(nouns.length)]}',
1 => '$artist arrested due to ${wordPairIterator.first.join(' ')}',
2 => '$artist releases ${capitalizePair(wordPairIterator.first)}',
3 => '$artist talks about his ${nouns[_random.nextInt(nouns.length)]}',
4 => '$artist talks about her ${nouns[_random.nextInt(nouns.length)]}',
5 => '$artist talks about their ${nouns[_random.nextInt(nouns.length)]}',
6 =>
'$artist says their music is inspired by ${wordPairIterator.first.join(' ')}',
7 =>
'$artist says the world needs more ${nouns[_random.nextInt(nouns.length)]}',
8 =>
'$artist calls their band ${adjectives[_random.nextInt(adjectives.length)]}',
9 =>
'$artist finally ready to talk about ${nouns[_random.nextInt(nouns.length)]}',
_ => 'Failed to generate news headline',
};
}
List<MaterialColor> getRandomColors(int amount) {
return List<MaterialColor>.generate(amount, (index) {
return _myListOfRandomColors[_random.nextInt(
_myListOfRandomColors.length,
)];
});
}
List<String> getRandomNames(int amount) {
return wordPairIterator
.take(amount)
.map((pair) => capitalizePair(pair))
.toList();
}
String capitalize(String word) {
return '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}';
}
String capitalizePair(WordPair pair) {
return '${capitalize(pair.first)} ${capitalize(pair.second)}';
}

View File

@@ -0,0 +1,373 @@
// 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/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// A simple widget that builds different things on different platforms.
class PlatformWidget extends StatelessWidget {
const PlatformWidget({
super.key,
required this.androidBuilder,
required this.iosBuilder,
});
final WidgetBuilder androidBuilder;
final WidgetBuilder iosBuilder;
@override
Widget build(context) {
assert(
defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS,
'Unexpected platform $defaultTargetPlatform',
);
return switch (defaultTargetPlatform) {
TargetPlatform.android => androidBuilder(context),
TargetPlatform.iOS => iosBuilder(context),
_ => const SizedBox.shrink(),
};
}
}
/// A platform-agnostic card with a high elevation that reacts when tapped.
///
/// This is an example of a custom widget that an app developer might create for
/// use on both iOS and Android as part of their brand's unique design.
class PressableCard extends StatefulWidget {
const PressableCard({
this.onPressed,
required this.color,
required this.flattenAnimation,
this.child,
super.key,
});
final VoidCallback? onPressed;
final Color color;
final Animation<double> flattenAnimation;
final Widget? child;
@override
State<StatefulWidget> createState() => _PressableCardState();
}
class _PressableCardState extends State<PressableCard>
with SingleTickerProviderStateMixin {
bool pressed = false;
late final AnimationController controller;
late final Animation<double> elevationAnimation;
@override
void initState() {
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 40),
);
elevationAnimation = controller.drive(
CurveTween(curve: Curves.easeInOutCubic),
);
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
double get flatten => 1 - widget.flattenAnimation.value;
@override
Widget build(context) {
return Listener(
onPointerDown: (details) {
if (widget.onPressed != null) {
controller.forward();
}
},
onPointerUp: (details) {
controller.reverse();
},
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
widget.onPressed?.call();
},
// This widget both internally drives an animation when pressed and
// responds to an external animation to flatten the card when in a
// hero animation. You likely want to modularize them more in your own
// app.
child: AnimatedBuilder(
animation: Listenable.merge([
elevationAnimation,
widget.flattenAnimation,
]),
child: widget.child,
builder: (context, child) {
return Transform.scale(
// This is just a sample. You likely want to keep the math cleaner
// in your own app.
scale: 1 - elevationAnimation.value * 0.03,
child: Padding(
padding:
const EdgeInsets.symmetric(
vertical: 16,
horizontal: 16,
) *
flatten,
child: PhysicalModel(
elevation:
((1 - elevationAnimation.value) * 10 + 10) * flatten,
borderRadius: BorderRadius.circular(12 * flatten),
clipBehavior: Clip.antiAlias,
color: widget.color,
child: child,
),
),
);
},
),
),
);
}
}
/// A platform-agnostic card representing a song which can be in a card state,
/// a flat state or anything in between.
///
/// When it's in a card state, it's pressable.
///
/// This is an example of a custom widget that an app developer might create for
/// use on both iOS and Android as part of their brand's unique design.
class HeroAnimatingSongCard extends StatelessWidget {
const HeroAnimatingSongCard({
required this.song,
required this.color,
required this.heroAnimation,
this.onPressed,
super.key,
});
final String song;
final Color color;
final Animation<double> heroAnimation;
final VoidCallback? onPressed;
double get playButtonSize => 50 + 50 * heroAnimation.value;
@override
Widget build(context) {
// This is an inefficient usage of AnimatedBuilder since it's rebuilding
// the entire subtree instead of passing in a non-changing child and
// building a transition widget in between.
//
// Left simple in this demo because this card doesn't have any real inner
// content so this just rebuilds everything while animating.
return AnimatedBuilder(
animation: heroAnimation,
builder: (context, child) {
return PressableCard(
onPressed: heroAnimation.value == 0 ? onPressed : null,
color: color,
flattenAnimation: heroAnimation,
child: SizedBox(
height: 250,
child: Stack(
alignment: Alignment.center,
children: [
// The song title banner slides off in the hero animation.
Positioned(
bottom: -80 * heroAnimation.value,
left: 0,
right: 0,
child: Container(
height: 80,
color: Colors.black12,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
song,
style: const TextStyle(
fontSize: 21,
fontWeight: FontWeight.w500,
),
),
),
),
// The play button grows in the hero animation.
Padding(
padding:
const EdgeInsets.only(bottom: 45) *
(1 - heroAnimation.value),
child: Container(
height: playButtonSize,
width: playButtonSize,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.black12,
),
alignment: Alignment.center,
child: Icon(
Icons.play_arrow,
size: playButtonSize,
color: Colors.black38,
),
),
),
],
),
),
);
},
);
}
}
/// A loading song tile's silhouette.
///
/// This is an example of a custom widget that an app developer might create for
/// use on both iOS and Android as part of their brand's unique design.
class SongPlaceholderTile extends StatelessWidget {
const SongPlaceholderTile({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 95,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
child: Row(
children: [
Container(
color: Theme.of(context).textTheme.bodyMedium!.color,
width: 130,
),
const Padding(padding: EdgeInsets.only(left: 12)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 9,
margin: const EdgeInsets.only(right: 60),
color: Theme.of(context).textTheme.bodyMedium!.color,
),
Container(
height: 9,
margin: const EdgeInsets.only(right: 20, top: 8),
color: Theme.of(context).textTheme.bodyMedium!.color,
),
Container(
height: 9,
margin: const EdgeInsets.only(right: 40, top: 8),
color: Theme.of(context).textTheme.bodyMedium!.color,
),
Container(
height: 9,
margin: const EdgeInsets.only(right: 80, top: 8),
color: Theme.of(context).textTheme.bodyMedium!.color,
),
Container(
height: 9,
margin: const EdgeInsets.only(right: 50, top: 8),
color: Theme.of(context).textTheme.bodyMedium!.color,
),
],
),
),
],
),
),
);
}
}
// ===========================================================================
// Non-shared code below because different interfaces are shown to prompt
// for a multiple-choice answer.
//
// This is a design choice and you may want to do something different in your
// app.
// ===========================================================================
/// This uses a platform-appropriate mechanism to show users multiple choices.
///
/// On Android, it uses a dialog with radio buttons. On iOS, it uses a picker.
void showChoices(BuildContext context, List<String> choices) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
showDialog<void>(
context: context,
builder: (context) {
int? selectedRadio = 1;
return AlertDialog(
contentPadding: const EdgeInsets.only(top: 12),
content: StatefulBuilder(
builder: (context, setState) {
return RadioGroup(
groupValue: selectedRadio,
onChanged: (value) {
setState(() => selectedRadio = value);
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: List<Widget>.generate(choices.length, (
index,
) {
return RadioListTile<int?>(
title: Text(choices[index]),
value: index,
);
}),
),
);
},
),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('CANCEL'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
return;
case TargetPlatform.iOS:
showCupertinoModalPopup<void>(
context: context,
builder: (context) {
return SizedBox(
height: 250,
child: CupertinoPicker(
backgroundColor: Theme.of(context).canvasColor,
useMagnifier: true,
magnification: 1.1,
itemExtent: 40,
scrollController: FixedExtentScrollController(
initialItem: 1,
),
children: List<Widget>.generate(choices.length, (index) {
return Center(
child: Text(
choices[index],
style: const TextStyle(fontSize: 21),
),
);
}),
onSelectedItemChanged: (value) {},
),
);
},
);
return;
default:
assert(false, 'Unexpected platform $defaultTargetPlatform');
}
}