android application design im praxischeck
DESCRIPTION
Android Application Design im Praxischeck Speaker: Lars Röwekamp Spätestens seit Android 4 ist Google der Sprung vom Smartphone aufs Tablet gelungen und somit ein echter Konkurrent für die iOS-Welt entstanden. Unmengen an APIs machen es dem Android-Entwickler einfach, die vielen Frameworkfeatures, wie Maps, Contacts, Kalender etc., in eigene Apps zu integrieren. Wie kommt es dann aber, dass nach wie vor ein Großteil der am Markt existierenden Apps eher schlecht als recht funktionieren und entsprechende Bewertungen bekommen? Die Session zeigt typische Pitfalls im Android Application Design und zeigt, wie man mit einer gut durchdachten Architektur und Ergonomie bei den Endanwendern punkten kann.TRANSCRIPT
@mobileLarson @_openKnowledge
Lars Röwekamp | CIO New Technologies
App Design im Praxis-Check
Kunde droht mit Auftrag ...
„Can you do this?“
Can we do this?
„Can you do this?“
„Yes, of course we can!“
„Can you do this?“
„But, lots of pitfalls ahead.“
„And, lots of work to do.“
35.000+ Produkte
5.000+ Änderungen p.m.
2000+ Produktgruppen
Tablets & Smartphones
15.000+ Bilder
In-App Navigation
Komplexe Suche
Pack mas’!
Problem: Sehr, sehr viele Daten
Lösung: No problèmo!
Leider doch. 50MB (+2x2GB)
> 50 MB (+ 2 x 5 GB)> APK Limit von 50 MB> Main Expension File (max. 2 GB)> Patch Expension File (max. 2 GB) !> Google Support via API> „automatischer“ Download im Hintergrund> Ablage in <shared-storage>/Android/…!
> Google Play Store Limits
Problem: Sehr, sehr viele Daten
> 50 MB (+ 2 x 5 GB)> APK Limit von 50 MB> Main Expension File (max. 2 GB)> Patch Expension File (max. 2 GB) !> Google Support via API> „automatischer“ Download im Hintergrund> Ablage in <shared-storage>/Android/…!
> Google Play Store Limits
Problem: Sehr, sehr viele Daten
> 50 MB (+ 2 x 5 GB)> APK Limit von 50 MB> Main Expension File (max. 2 GB)> Patch Expension File (max. 2 GB) !> Google Support via API> „automatischer“ Download im Hintergrund> Ablage in <shared-storage>/Android/…!
> Google Play Store Limits
Problem: Sehr, sehr viele Daten
> 50 MB (+ 2 x 5 GB)> APK Limit von 50 MB> Main Expension File (max. 2 GB)> Patch Expension File (max. 2 GB) !> Google Support via API> „automatischer“ Download im Hintergrund> Ablage in <shared-storage>/Android/…!
> Google Play Store Limits
Problem: Sehr, sehr viele Daten
Problem: Sehr, sehr viele Daten
Lösung: DB Download ...
> 200 MB+> Wifi Check inkl. optional Loading > Inkrementeller Download inkl. Resume
!> einige Minuten
> Vorab Hinweis auf „Ladezeit“ anzeigen> Working Progress Feedback inkl. „Step X of Y“> Loading Progress Feedback inkl. „X von Y“
> „manueller“ DB Download
Problem: Sehr, sehr viele Daten
> DB Download > Check for WiFi
<!-- WiFi check: Declare Wifi as needed -->! <manifest ...>!
<uses-feature android:name="android.hardware.wifi" ! android:required=["true"|"false"] />! ...! </manifest>
Problem: Sehr, sehr viele Daten
> DB Download > Check for WiFi only// WiFi check: check for existing WiFi!
! ConnectivityManager cm = (ConnectivityManager) ! getSystemService(Context.CONNECTIVITY_SERVICE);!! ! State wifi = ! cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)!
! ! ! ! .getState();!! if (wifi == NetworkInfo.State.CONNECTED || wifi == NetworkInfo.State.CONNECTING) {!
// WiFi is available!! } else { ! ...
Problem: Sehr, sehr viele Daten
Problem: Sehr, sehr viele Daten
// WiFi check: check for WiFi detail info! WifiManager wifiManager = (WifiManager) ! getSystemService(Context.WIFI_SERVICE);!! WifiInfo wifiInfo = wifiManager.getConnectionInfo();! SupplicantState wifiState = ! wifiInfo.getSupplicantState();!! if (wifiState == SupplicantState.COMPLETED) {!
// WiFi is available - get some wifiInfo details! int linkSpeed = wifiInfo.getLinkSpeed(); ...!! } else { ! ...
> DB Download > Check for WiFi Details
Problem: Sehr, sehr viele Daten
// WiFi check: check for WiFi detail info! WifiManager wifiManager = (WifiManager) ! getSystemService(Context.WIFI_SERVICE);!! WifiInfo wifiInfo = wifiManager.getConnectionInfo();! SupplicantState wifiState = ! wifiInfo.getSupplicantState();!! if (wifiState == SupplicantState.COMPLETED) {!
// WiFi is available - get some wifiInfo details! int linkSpeed = wifiInfo.getLinkSpeed(); ...!! } else { ! ...
> DB Download > Check for WiFi Details
> DB Download > Incremental Loading// Step 1: Prepare download with DownloadManager!
DownloadManager.Request downloadRequest = new ! DownloadManager.Request(Uri.parse(DOWNLOAD_URI));!
! // set additional optional information: ! // Title, Description, MimeType, ExternalFileDir, ...!
! // allow download via Mobile or Wifi! downloadRequest.setAllowedNetworkTypes( DownloadManager.Request.NETWORK_WIFI | ! DownloadManager.Request.NETWORK_MOBILE);!
! // do not show the file in "Downloads"! downloadRequest.setVisibleInDownloadsUi(false);
Problem: Sehr, sehr viele Daten
> DB Download > Incremental Loading!// Step 2: Start download with DownloadManager!
...! // register broadcast receiver for DOWNLOAD_COMPLETE! IntentFilter intentFilter = new IntentFilter(! DownloadManager.ACTION_DOWNLOAD_COMPLETE);! registerReceiver(receiver, intentFilter);!
! // Enqueue download! downloadId = downloadManager.enqueue(downloadRequest);
!!!!
Problem: Sehr, sehr viele Daten
> DB Download > Incremental Loading// Step 3: Do some stuff after download has finished !
BroadcastReceiver receiver = new BroadcastReceiver() { ! @Override public void onReceive(Context ctx, Intent intent) { // 1. DownloadManager.ACTION_DOWNLOAD_COMPLETE? // 2. ID = DownloadManager.EXTRA_DOWNLOAD_ID // 3. query DownloadManager for ID // 4. DownloadManager.STATUS_SUCCESSFUL? // 5. do some stuff with download } };
Problem: Sehr, sehr viele Daten
> DB Download > Showing Progress!!// show progress via DownloadManager (easy version)!
public void showDownload(View view) {! Intent intent = new Intent();! intent.setAction( DownloadManager.ACTION_VIEW_DOWNLOADS);! startActivity(i);! }
Problem: Sehr, sehr viele Daten
> DB Download > Showing Progress// show progress via own Activity (eXtended version)!
public class ProgressActivity extends Activity {!! !
private ProgressBar mProgress; // or ProgressDialog!private int mProgressStatus = 0;!private Handler mHandler = new Handler();!
!protected void onCreate(Bundle icicle) {! super.onCreate(icicle);! setContentView(R.layout.progressbar_activity); ! mProgress = (ProgressBar)findViewById(R.id.pb);!
// Start some work in background thread ! doWorkAndUpdateProgressBarInBackground(); !
} …!}
Problem: Sehr, sehr viele Daten
> DB Download > Showing Progress! // doWorkAndUpdateProgressBarInBackground() ! new Thread(new Runnable() {!! public void run() {!! while (mProgressStatus < 100) {!! mProgressStatus = checkDonwloadManagerStatus(); !!! // Update progress bar in UI thread!! mHandler.post(new Runnable() {!! public void run() {! ! mProgress.setProgress(mProgressStatus);!! }!! });!! }! }!}).start();
Problem: Sehr, sehr viele Daten
> DB Download > Showing Progress <!-- Spinning wheel -->! <ProgressBar! style="@android:style/Widget.ProgressBar.Large"! ... /> !! <!-- Progress bar from 0 .. 100 -->! <ProgressBar! style="@android:style/Widget.ProgressBar.Horizontal"! ... />
Problem: Sehr, sehr viele Daten
Problem: Sehr, sehr viele Daten
Lösung 2: DB Download +
> ca. 10 MB> Lazy Loading Ansatz zum Füllen der DB > Nur Basisdaten initial laden/vorhalten > Produktdetails und Bilder „as needed“
!> Unterstützte Funktionen
> Home Page Angebote anzeigen> Kategorien anzeigen> Suchen1), Filtern, ...
> DB Download+
Problem: Sehr, sehr viele Daten
> ca. 10 MB> Lazy Loading Ansatz zum Füllen der DB > Nur Basisdaten initial laden/vorhalten > Produktdetails und Bilder „as needed“
!> Unterstützte Funktionen
> Home Page Angebote anzeigen> Kategorien anzeigen> Suchen1), Filtern, ...
> DB Download+ 1) Online-/Offline-Suche
Problem: Sehr, sehr viele Daten
> DB Download+
Problem: Sehr, sehr viele Daten
> DB Download+
Problem: Sehr, sehr viele Daten
> Picasso (Square / komfortable API)> UrlImageViewHelper (sehr schnell)> Volley (Google / gut durchdacht)> Android Universal Image Loader (most popular)!
> „Do it yourself“ vs. „3rd Party“
Problem: Sehr, sehr viele Daten
Problem: Sehr, sehr viele Updates
Lösung: Update-Protokoll
> ca. 5000 Änderungen p.M. > Modification XML Info zum „Start-up“ laden > Neue/geänderte/gelöschte Produkte/Bilder
!> Protokoll (XML basiert)
> Gelöschte Produkte/Bilder löschen> Neue Produkte/Bilder laden> Geänderte Produkte/Bilder laden
> Update Protokoll
Problem: Sehr, sehr viele Updates
Problem: Sehr, sehr viele Updates
Lösung 2: Update-Protokoll+
> ca. 5000 Änderungen p.M. > Modification Info zum „Start-up“ laden > Produktdetails „as needed“
!> Protokoll (JSON basiert)
> Neue Produkte als Dummy anlegen> Gelöschte Produkte markieren> Geänderte Produkte markieren > Aufräumen „on-the-fly“
> Update Protokoll+
Problem: Sehr, sehr viele Updates
> ca. 5000 Änderungen p.M. > Modification Info zum „Start-up“ laden > Produktdetails „as needed“
!> Protokoll (JSON basiert)
> Neue Produkte als Dummy anlegen> Gelöschte Produkte markieren> Geänderte Produkte markieren > Aufräumen „on-the-fly“
> Update Protokoll+
Problem: Sehr, sehr viele Updates
Problem: Sehr, sehr viele Updates
> JSON
Problem: Sehr, sehr viele Updates
> JSON
Problem: Sehr, sehr viele Updates
> JSON
Problem: Sehr, sehr viele Updates
> JSON
Problem: Lesbare Datenbank
Lösung: brauche ich eine?
Problem: Lesbare Datenbank
Problem: Lesbare Datenbank
Lösung: Encryption
> DB im Android Filesystem> DB Verschlüsselung via SQLCipher > 256-bit AES Encryption für SQLite> Proxies für wichtigsten Android DB Klassen
!> SQLCipher anwenden
> Step 1: Imports setzen> Step 2: SQLCipher Libs laden> Step 3: alles wie sonst auch
> Encryption
Problem: Lesbare Datenbank
> Encryption// Step 1: import sqlcipher!import info.guardianproject.database.! sqlcipher.SQLDatabase; !!// Trigger database initialization!public class SqlCypherActivity extends Activity {!
...! }
Problem: Lesbare Datenbank
> Encryption// Trigger database initialization!public class SqlCypherActivity extends Activity {!
! ! @Override! public void onCreate(Bundle savedInstanceState) {! super.onCreate(savedInstanceState);! setContentView(R.layout.main); ! initializeSQLCipher();! }! ...!
}
Problem: Lesbare Datenbank
> Encryption// Trigger database initialization!public class SqlCypherActivity extends Activity {!
! private void InitializeSQLCipher() {!
// Step 2: Load SQLCipher Libs! SQLiteDatabase.loadLibs(this);!
!// Step 3: Business as usual!
...!} !...!!
}
Problem: Lesbare Datenbank
Problem: Diversifikation
Lösung: Ignorieren
4.310 %
4.217 %
4.135,3 %
4.015,2 %
2.319 %
2.22.33.x4.04.14.24.34.4
Version Problem: Diversifikation
xlarge4,9 %large
7,7 %
middle79 %
small8 %
smallmiddlelargexlarge
Größe Problem: Diversifikation
xxhdpi12 %
xhdpi20,7 %
hdpi34,6 %
mdpi22 %
ldpi9 %
ldpimdpitvdpihdpixhdpixxhdpi
Problem: Diversifikation Auflösung
Lösung 2: Teilweise ignorieren
Problem: Diversifikation
4.310 %
4.217 %
4.135,3 %
4.015,2 %
2.319 %
2.22.33.x4.04.14.24.34.4
Version Problem: Diversifikation
Lösung 3: Unified UI
Problem: Diversifikation
> Unified UI
Problem: Diversifikation
> Unified UI
Problem: Diversifikation
> wäre schön, wären da nicht die Versionen !> Step 1: wenn möglich kompatibel bleiben > Step 2: wenn möglich Support Library > Step 3: wenn möglich 3rd Party Lösungen > Step 4: wenn möglich „Weiche“ !> Step 5: WTF! Verschiedene APKs !
!
> Unified UI
Problem: Diversifikation Version
> unterstützt Abwärtskompabilität > v4 für Android 1.6 und höher > v7 für Android 2.1 und höher > v8 für Android 2.2 und höher > v13 für Android 3.2 und höher !
!
!
!
> Unified UI via Support Libraries
Problem: Diversifikation Version
> Fragment (v4) > NotificationCombat (v4) > LocalBroadcastManager (v4) > ViewPager (v4) > DrawerLayout (v4) > SlidingPaneLayout (v4) > Loader (v4) !
!
> Unified UI via Support Libraries
Problem: Diversifikation Version
> ActionBar (v7) > ActionBarActivity (v7) > ShareActionProvider (v7) > GridLayout (v7) > MediaRouter (v7) > RenderScript (v8) > FragmentCombat (v13) !
!
> Unified UI via Support Libraries
Problem: Diversifikation Version
> ActionBarSherlock (Android 2.x) !
> Unified UI via 3rd Party Libraries
Problem: Diversifikation Version
> Unified UI via „Versions-Weiche“
Version
!// check version of current device!
int apiVersion = android.os.Build.VERSION.SDK_INT;! if (apiVersion >= android.os.Build.VERSION_CODES.FROYO){! // Do something for froyo and above versions! } else{! // do something for phones running an SDK before froyo! }
Problem: Diversifikation
> Unified UI via „Feature-Weiche“
Version
!// check version of current device!
PackageManager packageManager = this.getPackageManager();!! if (packageManager.hasSystemFeature(! PackageManager.FEATURE_NFC)) {!
// NFC feature is available !! ! ...!! } else { ! // NFC feature is NOT available !! ! ...!! }
Problem: Diversifikation
Lösung 4: Multi-Device UI
Problem: Diversifikation
> Multi-Device UI
Problem: Diversifikation
> Multi-Device UI
Problem: Diversifikation
> UI Fragments als „Building Blocks“ > UI Activity Layout als „Block Assembler“ > UI Activity Class als „Block Assembler Logic“ !> UI Ressources als „Switch“ !
Größe
> Multi-Device UI
Problem: Diversifikation Größe
> Activity Modul a.k.a. Sub Activity > inkl. eigener UI > inkl. eigenem Lifecycle !> benötigt immer eine umliegende Activity > Lifecycle passt sich Activity-Lifecycle an
> Fragment
Problem: Diversifikation Größe
> Size: small, normal, large, xlarge > Density: ldpi, mdpi, hdpi, xhdpi > Orientation: landscape, portrait
> Designation: sw, w, h, ...dp !
> Ressources
Problem: Diversifikation Größe
> Size: small, normal, large, xlarge > Density: ldpi, mdpi, hdpi, xhdpi > Orientation: landscape, portrait
> Designation: sw, w, h, ...dp !
> Ressources
Problem: Diversifikation
Deprecated
Added
Größe
Größe
> Multi-Device UI in Aktion
Problem: Diversifikation
Größe
> Multi-Device UI in Aktion
Problem: Diversifikation
Problem: In-App Navigation
Lösung: In-App Navigation ;-)
> Activity Stack
Problem: In-App Navigation
> Activity Stack
> Intent Flags> FLAG_ACTIVITY_NEW_TASK> FLAG_ACTIVITY_SINGLE_TOP> FLAG_ACTIVITY_CLEAR_TOP
> Launch Mode> standard> singleTop> singleTask> singleInstance
Problem: In-App Navigation
> Activity Stack
Problem: In-App Navigation
> Back vs. Up
Problem: In-App Navigation
> Back vs. Up> „Back“ berücksichtigt Activity Stack > „Up“ berücksichtigt Use Case Stack
!> In-App Navigation
> Up-Navigation via In-App Buttons> Manipulation des Activity Stacks> Für Home und „Main Activities“
> In-App Navigation
Problem: In-App Navigation
Problem: In-App Navigation
Problem: In-App Navigation
Problem: In-App Navigation
Problem: In-App Navigation
Deep Dive
Problem: In-App Navigation!!@Override
public void onCreate(Bundle savedInstanceState) {! super.onCreate(savedInstanceState);!!! setContentView(R.layout.main);!!! ActionBar actionBar = getActionBar();!! actionBar.setDisplayHomeAsUpEnabled(true);!! ...!}
Step 1: Enable „Up Navigation“
Problem: In-App Navigation!!@Override!public boolean onOptionsItemSelected(MenuItem item) {! switch (item.getItemId()) {!! // app icon in action bar clicked: go home! case android.R.id.home:! Intent i = new Intent(this, MainActivity.class);! i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);! startActivity(intent);! return true;!! default:! return super.onOptionsItemSelected(item);! } !}
Step 2: Realize „Up Navigation“
> … ziemlich viel Aufwand, oder? > … was ist mit „Deep-Dive“ Intents?
> Eigentlich ganz einfach, aber …
Problem: In-App Navigation
Problem: In-App Navigation!!!<activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> ... </intent-filter> </activity> !<activity android:name=".ResultActivity" android:parentActivityName=".MainActivity"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".MainActivity"/> </activity>
Step 1: Enable „Up Navigation“
Problem: In-App Navigation!!@Override!public boolean onOptionsItemSelected(MenuItem item) {! switch (item.getItemId()) {!! // app icon in action bar clicked: go home! case android.R.id.home:! Intent i = new Intent(this, MainActivity.class);! i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);! startActivity(intent);! return true;! ... ! default:! return super.onOptionsItemSelected(item);! } !}
Step 2: Realize „Up Navigation“
> Einsprung von außen „mitten“ in die App> Navigation Stack wird künstlich aufgebaut a.k.a. Synthetic Back Stack!> Was ist mit dem fehlenden Kontext? „Music Details“ navigiert zurück zur „Music Liste“, aber zu welcher?
> „Deep-Dive“ Intents
Problem: In-App Navigation
Problem: In-App Navigation!!@Override
public void onPrepareNavigateUpTaskStack(! TaskStackBuilder builder) {!! // retrieve intent for manipulation! int position = ...; ! Intent intent = builder.getIntentAt(position);!! // manipulate intent ! intent.putExtra(INTENT_SPECIFIC_INFO, specificInfo); ! ...!}
Step 3: Manipulate Intent Data
@mobileLarson @_openKnowledge
Lars Röwekamp | CIO New Technologies
Thanks! Questions?
Demo