Skip to content

Commit

Permalink
Add a Material/Cupertino adaptive application example (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
xster authored and RedBrogdon committed Jun 10, 2019
1 parent 08beb69 commit 325c5a5
Show file tree
Hide file tree
Showing 67 changed files with 2,718 additions and 0 deletions.
70 changes: 70 additions & 0 deletions platform_design/.gitignore
@@ -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
10 changes: 10 additions & 0 deletions platform_design/.metadata
@@ -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
104 changes: 104 additions & 0 deletions platform_design/README.md
@@ -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 added platform_design/adaptive-overview.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions platform_design/android/app/build.gradle
@@ -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'
}
7 changes: 7 additions & 0 deletions platform_design/android/app/src/debug/AndroidManifest.xml
@@ -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>
33 changes: 33 additions & 0 deletions platform_design/android/app/src/main/AndroidManifest.xml
@@ -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>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions platform_design/android/app/src/main/res/values/styles.xml
@@ -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>
7 changes: 7 additions & 0 deletions platform_design/android/app/src/profile/AndroidManifest.xml
@@ -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>
29 changes: 29 additions & 0 deletions platform_design/android/build.gradle
@@ -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
}
1 change: 1 addition & 0 deletions platform_design/android/gradle.properties
@@ -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
15 changes: 15 additions & 0 deletions platform_design/android/settings.gradle
@@ -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 comments on commit 325c5a5

Please sign in to comment.