Add a Material/Cupertino adaptive application example (#69)

pull/89/head
xster 5 years ago committed by Andrew Brogdon
parent 08beb69245
commit 325c5a5d2b

@ -0,0 +1,70 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# Visual Studio Code related
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.packages
.pub-cache/
.pub/
/build/
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

@ -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: 8bea3fb2ebadc3933b6b213483d2d4379ac53a5c
channel: master
project_type: app

@ -0,0 +1,104 @@
# Platform Design
Instead of transliterating widgets one by one between Cupertino and Material,
Android and iOS apps often follow different information architecture patterns
that require some design decisions.
This sample project shows a Flutter app that maximizes application code reuse
while adhering to different design patterns on Android and iOS. On
Android, it uses Material's [lateral navigation](https://material.io/design/navigation/understanding-navigation.html#types-of-navigation)
based on a drawer and on iOS, it adheres to Apple Human Interface Guideline's
[flat navigation](https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/navigation/)
by using a bottom tab bar.
Visually, the app presents platform-agnostic content surrounded by
platform-specific 'chrome'.
# Preview
![App's platform toggling preview](adaptive-overview.gif)
See https://youtu.be/svhbbFZg1IA for a longer non-gif format.
# Features
## Home
Defines the top level navigation structure of the app and shows the contents
of the songs tab on launch.
### Android
* Uses the drawer paradigm on the root page.
### iOS
* Uses bottom tab bars with parallel navigation stacks.
## Songs feed tab
Shows platform-agnostic cards that is tappable and that performs a hero
transition on top of the platform native page transitions.
Both platforms also show a button in their app/nav bar to toggle the platform.
### Android
* Android uses a static pull-to-refresh pattern with an additional refresh
button in the app bar.
* The song details page must be popped in order to change tabs on Android.
### iOS
* The iOS songs tab uses a scrollable iOS 11 large title style navigation bar.
* iOS uses an overscrolling pull-to-refresh pattern.
* On iOS, parallel tabs are always accessible and the songs tab's navigation
stack is preserved when changing tabs.
## News Tab
Shows platform-agnostic news boxes.
### Android
* The news tab always appears on top of the songs tab when summoned from the
drawer.
### iOS
* The news tab appears instead of the songs tab on iOS when switching tabs from
the tab bar.
## Profile Tab
Shows a number of user preferences.
### Android
* The profile tab appears on top of the songs tab on Android.
* Has tappable preference cards which shows a multiple-choice dialog on Android.
* The log out button shows a 2 button dialog on Android.
### iOS
* The profile tab appears instead of the songs tab on iOS.
* Has tappable preference cards which shows a picker on iOS.
* The log out button shows a 3 choice action sheet on iOS.
## Settings Tab
Shows a number of app settings via Material switches which auto adapt to the
platform.
### Android
* The settings is directly available in the drawer on Android since a Material
Design drawer can fit many tabs.
### iOS
* The settings is accessible from a button inside the profile tab's nav bar on
iOS. This is a common pattern since there are conventionally more items in the
drawer than there are tabs.
* On iOS, the settings page is shown as a full screen dialog instead of a tab
in the tab scaffold.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

@ -0,0 +1,61 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 28
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.platform_design"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.platform_design">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

@ -0,0 +1,33 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.platform_design">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="platform_design"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

@ -0,0 +1,13 @@
package com.example.platform_design;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
}
}

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.platform_design">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

@ -0,0 +1,29 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

@ -0,0 +1 @@
org.gradle.jvmargs=-Xmx1536M

@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip

@ -0,0 +1,15 @@
include ':app'
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>8.0</string>
</dict>
</plist>

@ -0,0 +1 @@
#include "Generated.xcconfig"

@ -0,0 +1 @@
#include "Generated.xcconfig"

@ -0,0 +1,506 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B80C3931E831B6300D905FE /* App.framework */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEBA1CF902C7004384FC /* Flutter.framework */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
);
path = Runner;
sourceTree = "<group>";
};
97C146F11CF9000F007C117D /* Supporting Files */ = {
isa = PBXGroup;
children = (
97C146F21CF9000F007C117D /* main.m */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0910;
ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
97C146F31CF9000F007C117D /* main.m in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = S8QB4VV633;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.platformDesign;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.platformDesign;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.platformDesign;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0910"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildSystemType</key>
<string>Original</string>
</dict>
</plist>

@ -0,0 +1,6 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate
@end

@ -0,0 +1,13 @@
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="2GX-QC-rKP"/>
<viewControllerLayoutGuide type="bottom" id="0si-XA-m9X"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Efu-BU-mRs">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="76.811594202898561" y="251.11607142857142"/>
</scene>
</scenes>
</document>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>platform_design</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

@ -0,0 +1,9 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

@ -0,0 +1,186 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'songs_tab.dart';
import 'news_tab.dart';
import 'profile_tab.dart';
import 'settings_tab.dart';
import 'widgets.dart';
void main() => runApp(MyAdaptingApp());
class MyAdaptingApp extends StatelessWidget {
@override
Widget build(context) {
// Change this value to better see animations.
timeDilation = 1;
// 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,
),
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: CupertinoThemeData(),
child: Material(child: child),
);
},
home: 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 {
@override
_PlatformAdaptingHomePageState 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 reparents 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(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(context) {
return CupertinoTabScaffold(
tabBar: CupertinoTabBar(
items: [
BottomNavigationBarItem(title: Text(SongsTab.title), icon: SongsTab.iosIcon),
BottomNavigationBarItem(title: Text(NewsTab.title), icon: NewsTab.iosIcon),
BottomNavigationBarItem(title: Text(ProfileTab.title), icon: ProfileTab.iosIcon),
],
),
tabBuilder: (context, index) {
switch (index) {
case 0:
return CupertinoTabView(
defaultTitle: SongsTab.title,
builder: (context) => SongsTab(key: songsTabKey),
);
case 1:
return CupertinoTabView(
defaultTitle: NewsTab.title,
builder: (context) => NewsTab(),
);
case 2:
return CupertinoTabView(
defaultTitle: ProfileTab.title,
builder: (context) => ProfileTab(),
);
default:
assert(false, 'Unexpected tab');
return null;
}
},
);
}
@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: 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: Text(SongsTab.title),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
leading: NewsTab.androidIcon,
title: Text(NewsTab.title),
onTap: () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(
builder: (context) => NewsTab()
));
},
),
ListTile(
leading: ProfileTab.androidIcon,
title: Text(ProfileTab.title),
onTap: () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(
builder: (context) => ProfileTab()
));
},
),
// Long drawer contents are often segmented.
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Divider(),
),
ListTile(
leading: SettingsTab.androidIcon,
title: Text(SettingsTab.title),
onTap: () {
Navigator.pop(context);
Navigator.push(context, MaterialPageRoute(
builder: (context) => SettingsTab()
));
},
),
],
),
);
}
}

@ -0,0 +1,126 @@
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);
@override
_NewsTabState createState() => _NewsTabState();
}
class _NewsTabState extends State<NewsTab> {
static const _itemsLength = 20;
List<Color> colors;
List<String> titles;
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(context, index) {
if (index >= _itemsLength)
return null;
return SafeArea(
top: false,
bottom: false,
child: Card(
elevation: 1.5,
margin: 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: TextStyle(color: Colors.white),
),
),
Padding(padding: EdgeInsets.only(left: 16)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
titles[index],
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
Padding(padding: EdgeInsets.only(top: 8)),
Text(
contents[index],
),
],
),
),
],
),
),
)
),
);
}
// ===========================================================================
// Non-shared code below because this tab uses different scaffolds.
// ===========================================================================
Widget _buildAndroid(context) {
return Scaffold(
appBar: AppBar(
title: Text(NewsTab.title),
),
body: Container(
color: Colors.grey[100],
child: ListView.builder(
itemBuilder: _listBuilder,
),
),
);
}
Widget _buildIos(context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Container(
color: Colors.grey[100],
child: ListView.builder(
itemBuilder: _listBuilder,
),
),
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

@ -0,0 +1,243 @@
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);
Widget _buildBody(context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
Padding(
padding: EdgeInsets.all(8),
child: Center(
child: Text('😼', style: TextStyle(
fontSize: 80,
decoration: TextDecoration.none,
)),
),
),
PreferenceCard(
header: 'MY INTENSITY PREFERENCE',
content: '🔥',
preferenceChoices: [
'Super heavy',
'Dial it to 11',
"Head bangin'",
'1000W',
'My neighbor hates me',
],
),
PreferenceCard(
header: 'CURRENT MOOD',
content: '🤘🏾🚀',
preferenceChoices: [
'Over the moon',
'Basking in sunlight',
'Hello fellow Martians',
'Into the darkness',
],
),
Expanded(
child: Container(),
),
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(context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: _buildBody(context),
);
}
Widget _buildIos(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(
CupertinoPageRoute(
title: SettingsTab.title,
fullscreenDialog: true,
builder: (context) => SettingsTab(),
),
);
},
),
),
child: _buildBody(context),
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}
class PreferenceCard extends StatelessWidget {
const PreferenceCard({ this.header, this.content, this.preferenceChoices });
final String header;
final String content;
final List<String> preferenceChoices;
@override
Widget build(context) {
return PressableCard(
color: Colors.green,
flattenAnimation: AlwaysStoppedAnimation(0),
child: Stack(
children: [
Container(
height: 120,
width: 250,
child: Padding(
padding: EdgeInsets.only(top: 40),
child: Center(
child: Text(
content,
style: TextStyle(fontSize: 48),
),
),
),
),
Positioned(
top: 0,
left: 0,
right: 0,
child: Container(
color: Colors.black12,
height: 40,
padding: EdgeInsets.only(left: 12),
alignment: Alignment.centerLeft,
child: Text(
header,
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
onPressed: () {
showChoices(context, preferenceChoices);
},
);
}
}
class LogOutButton extends StatelessWidget {
static const _logoutMessage = Text('You may check out any time you like, but you can never leave');
// ===========================================================================
// 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(context) {
return RaisedButton(
child: 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(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Log out?'),
content: _logoutMessage,
actions: [
FlatButton(
child: const Text('Go back'),
onPressed: () => Navigator.pop(context) ,
),
FlatButton(
child: const Text('Cancel'),
onPressed: () => Navigator.pop(context),
),
],
);
}
);
},
);
}
Widget _buildIos(context) {
return CupertinoButton(
color: CupertinoColors.destructiveRed,
child: 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(
context: context,
builder: (context) {
return CupertinoActionSheet(
title: Text('Log out?'),
message: _logoutMessage,
actions: [
CupertinoActionSheetAction(
child: const Text('Reprogram the night man'),
isDestructiveAction: true,
onPressed: () => Navigator.pop(context),
),
CupertinoActionSheetAction(
child: const Text('Go back'),
onPressed: () => Navigator.pop(context),
),
],
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
isDefaultAction: true,
onPressed: () => Navigator.pop(context),
),
);
},
);
},
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

@ -0,0 +1,104 @@
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);
@override
_SettingsTabState 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: [
Padding(padding: EdgeInsets.only(top: 24)),
ListTile(
title: 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: Text('Enable notifications'),
trailing: Switch.adaptive(
value: switch2,
onChanged: (value) => setState(() => switch2 = value),
),
),
ListTile(
title: Text('Remind me to rate this app'),
trailing: Switch.adaptive(
value: switch3,
onChanged: (value) => setState(() => switch3 = value),
),
),
ListTile(
title: Text('Background song refresh'),
trailing: Switch.adaptive(
value: switch4,
onChanged: (value) => setState(() => switch4 = value),
),
),
ListTile(
title: Text('Recommend me songs based on my location'),
trailing: Switch.adaptive(
value: switch5,
onChanged: (value) => setState(() => switch5 = value),
),
),
ListTile(
title: Text('Auto-transition playback to cast devices'),
trailing: Switch.adaptive(
value: switch6,
onChanged: (value) => setState(() => switch6 = value),
),
),
ListTile(
title: 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(context) {
return Scaffold(
appBar: AppBar(
title: Text(SettingsTab.title),
),
body: _buildList(),
);
}
Widget _buildIos(context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: _buildList(),
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

@ -0,0 +1,99 @@
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({ this.id, this.song, this.color });
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: 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,
);
},
),
Divider(
height: 0,
color: Colors.grey,
),
Expanded(
child: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
if (index == 0) {
return Padding(
padding: const EdgeInsets.only(left: 15, top: 16, bottom: 8),
child: Text('You might also like:', style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
)),
);
}
// Just a bunch of boxes that simulates loading song choices.
return SongPlaceholderTile();
},
),
),
],
),
);
}
// ===========================================================================
// Non-shared code below because we're using different scaffolds.
// ===========================================================================
Widget _buildAndroid(context) {
return Scaffold(
appBar: AppBar(title: Text(song)),
body: _buildBody(),
);
}
Widget _buildIos(context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text(song),
previousPageTitle: 'Songs',
),
child: _buildBody(),
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

@ -0,0 +1,168 @@
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({ Key key, this.androidDrawer }) : super(key: key);
final Widget androidDrawer;
@override
_SongsTabState createState() => _SongsTabState();
}
class _SongsTabState extends State<SongsTab> {
static const _itemsLength = 50;
final _androidRefreshKey = GlobalKey<RefreshIndicatorState>();
List<MaterialColor> colors;
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(context, index) {
if (index >= _itemsLength)
return null;
// 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: AlwaysStoppedAnimation(0),
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
builder: (context) => SongDetailTab(
id: index,
song: songNames[index],
color: color,
),
)),
),
),
);
}
void _togglePlatform() {
TargetPlatform _getOppositePlatform() {
if (defaultTargetPlatform == TargetPlatform.iOS) {
return TargetPlatform.android;
} else {
return TargetPlatform.iOS;
}
}
debugDefaultTargetPlatformOverride = _getOppositePlatform();
// 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 differenc 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(context) {
return Scaffold(
appBar: AppBar(
title: Text(SongsTab.title),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () async => await _androidRefreshKey.currentState.show(),
),
IconButton(
icon: Icon(Icons.shuffle),
onPressed: _togglePlatform,
),
],
),
drawer: widget.androidDrawer,
body: RefreshIndicator(
key: _androidRefreshKey,
onRefresh: _refreshData,
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 12),
itemBuilder: _listBuilder,
),
),
);
}
Widget _buildIos(context) {
return CustomScrollView(
slivers: [
CupertinoSliverNavigationBar(
trailing: CupertinoButton(
padding: EdgeInsets.zero,
child: Icon(CupertinoIcons.shuffle),
onPressed: _togglePlatform,
),
),
CupertinoSliverRefreshControl(
onRefresh: _refreshData,
),
SliverSafeArea(
top: false,
sliver: SliverPadding(
padding: EdgeInsets.symmetric(vertical: 12),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(_listBuilder),
),
),
),
],
);
}
@override
Widget build(context) {
return PlatformWidget(
androidBuilder: _buildAndroid,
iosBuilder: _buildIos,
);
}
}

@ -0,0 +1,92 @@
import 'dart:math';
import 'package:english_words/english_words.dart';
// This reimplements generateWordPair because english_words's
// implementation has some performance issues.
// https://github.com/filiph/english_words/issues/9
// ignore: implementation_imports
import 'package:english_words/src/words/unsafe.dart';
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();
Iterable wordPairIterator = generateWordPair();
Iterable<WordPair> generateWordPair() sync* {
bool filterWord(word) => unsafe.contains(word);
String pickRandom(List<String> list) => list[_random.nextInt(list.length)];
String prefix;
while (true) {
if (_random.nextBool()) {
prefix = pickRandom(adjectives);
} else {
prefix = pickRandom(nouns);
}
final suffix = pickRandom(nouns);
if (filterWord(prefix) || filterWord(suffix))
continue;
final wordPair = WordPair(prefix, suffix);
yield wordPair;
}
}
String generateRandomHeadline() {
final artist = capitalizePair(wordPairIterator.first);
switch (_random.nextInt(9)) {
case 0:
return '$artist says ${nouns[_random.nextInt(nouns.length)]}';
case 1:
return '$artist arrested due to ${wordPairIterator.first.join(' ')}';
case 2:
return '$artist releases ${capitalizePair(wordPairIterator.first)}';
case 3:
return '$artist talks about his ${nouns[_random.nextInt(nouns.length)]}';
case 4:
return '$artist talks about her ${nouns[_random.nextInt(nouns.length)]}';
case 5:
return '$artist talks about their ${nouns[_random.nextInt(nouns.length)]}';
case 6:
return '$artist says their music is inspired by ${wordPairIterator.first.join(' ')}';
case 7:
return '$artist says the world needs more ${nouns[_random.nextInt(nouns.length)]}';
case 7:
return '$artist calls their band ${adjectives[_random.nextInt(adjectives.length)]}';
case 8:
return '$artist finally ready to talk about ${nouns[_random.nextInt(nouns.length)]}';
}
assert(false, 'Failed to generate news headline');
return null;
}
List<MaterialColor> getRandomColors(int amount) {
return List<MaterialColor>.generate(amount, (int 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)}';
}

@ -0,0 +1,334 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
/// A simple widget that builds different things on different platforms.
class PlatformWidget extends StatelessWidget {
const PlatformWidget({
Key key,
@required this.androidBuilder,
@required this.iosBuilder,
}) : assert(androidBuilder != null),
assert(iosBuilder != null),
super(key: key);
final WidgetBuilder androidBuilder;
final WidgetBuilder iosBuilder;
@override
Widget build(context) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return androidBuilder(context);
case TargetPlatform.iOS:
return iosBuilder(context);
default:
assert(false, 'Unexpected platform $defaultTargetPlatform');
return null;
}
}
}
/// 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,
this.color,
this.flattenAnimation,
this.child,
});
final VoidCallback onPressed;
final Color color;
final Animation<double> flattenAnimation;
final Widget child;
@override
State<StatefulWidget> createState() => new _PressableCardState();
}
class _PressableCardState extends State<PressableCard> with SingleTickerProviderStateMixin {
bool pressed = false;
AnimationController controller;
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: () {
if (widget.onPressed != null) {
widget.onPressed();
}
},
// 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: 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 {
HeroAnimatingSongCard({ this.song, this.color, this.heroAnimation, this.onPressed });
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: EdgeInsets.symmetric(horizontal: 12),
child: Text(
song,
style: TextStyle(
fontSize: 21,
fontWeight: FontWeight.w500,
),
),
),
),
// The play button grows in the hero animation.
Padding(
padding: EdgeInsets.only(bottom: 45) * (1 - heroAnimation.value),
child: Container(
height: playButtonSize,
width: playButtonSize,
decoration: 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 {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 95,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 8),
child: Row(
children: [
Container(
color: Colors.grey[400],
width: 130,
),
Padding(
padding: EdgeInsets.only(left: 12),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 9,
margin: EdgeInsets.only(right: 60),
color: Colors.grey[300],
),
Container(
height: 9,
margin: EdgeInsets.only(right: 20, top: 8),
color: Colors.grey[300],
),
Container(
height: 9,
margin: EdgeInsets.only(right: 40, top: 8),
color: Colors.grey[300],
),
Container(
height: 9,
margin: EdgeInsets.only(right: 80, top: 8),
color: Colors.grey[300],
),
Container(
height: 9,
margin: EdgeInsets.only(right: 50, top: 8),
color: Colors.grey[300],
),
],
),
),
],
),
),
);
}
}
// ===========================================================================
// 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(
context: context,
builder: (context) {
int selectedRadio = 1;
return AlertDialog(
contentPadding: EdgeInsets.only(top: 12),
content: StatefulBuilder(
builder: (context, setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: List<Widget>.generate(choices.length, (index) {
return RadioListTile(
title: Text(choices[index]),
value: index,
groupValue: selectedRadio,
onChanged: (value) {
setState(() => selectedRadio = value);
},
);
}),
);
},
),
actions: [
FlatButton(
child: Text('OK'),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('CANCEL'),
onPressed: () => Navigator.of(context).pop(),
),
],
);
},
);
return;
case TargetPlatform.iOS:
showCupertinoModalPopup(
context: context,
builder: (context) {
return SizedBox(
height: 250,
child: CupertinoPicker(
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: TextStyle(
fontSize: 21,
),
));
}),
onSelectedItemChanged: (value) {},
),
);
}
);
return;
default:
assert(false, 'Unexpected platform $defaultTargetPlatform');
}
}

@ -0,0 +1,22 @@
name: platform_design
description: A project showcasing a Flutter app following different platform IA conventions.
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
flutter: ">=1.5.2"
dependencies:
english_words: ^3.1.5
flutter_lorem: ^1.1.0
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true

@ -0,0 +1,30 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:platform_design/main.dart';
void main() {
testWidgets('Can change platform correctly', (tester) async {
await tester.pumpWidget(MyAdaptingApp());
// The test should be able to find the drawer button.
expect(find.byIcon(Icons.menu), findsOneWidget);
// There should be a refresh button.
expect(find.byIcon(Icons.refresh), findsOneWidget);
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
await tester.pumpWidget(MyAdaptingApp());
// There should now be a large title style nav bar.
expect(find.byType(CupertinoSliverNavigationBar), findsOneWidget);
// There's a tab button for the first tab.
expect(find.byIcon(CupertinoIcons.music_note), findsOneWidget);
// The hamburger button isn't there anymore.
expect(find.byIcon(Icons.menu), findsNothing);
// Since this is a static, undo the change made in the test.
debugDefaultTargetPlatformOverride = null;
});
}

@ -7,6 +7,7 @@ declare -a PROJECT_NAMES=(
"veggieseasons" \
"place_tracker" \
"platform_view_swift" \
"platform_design"
)
for PROJECT_NAME in "${PROJECT_NAMES[@]}"

Loading…
Cancel
Save