Last week I decided I would create a little micro Android app and release it so that I could get experience with the process. I recently came to the opinion that it's a good idea to hit learning curves with something small first rather than attempting both “big” and “new” at the same time. So I created an Android app called BPM Tapper. What I found surprising was that I went from vague idea to 95% code complete in 2 days, but then it took another 5 or 6 days to get it released.
Now, my goal here with this blog post is twofold. Firstly, to document what I went through releasing my first Android application so that in future I remember everything that needs to be done; great for planning. Secondly, I want to document tips and strategies to improve next time.
1. Android Completeness
By Android Completeness I'm referring to things that are either required or expected of common Android applications. The tasks I came across are as follows.
1.1. Inspecting Android Lint warnings.
This is an easy one. The Android SDK provides a lint tool that analyses your compiled code and resources, and creates a list of warnings. Not all Lint warnings are correct but they all should be vetted and resolved if necessary.
1.2. Handling interruption.
Your app can be interrupted at any time, e.g. if the phone gets a call while your app is in use, the OS will kick your app out and data will be lost if you don't handle it. Therefore if you don't want users to lose their on-screen state when using your app (or changing orientations), you generally need to implement two methods in your activities. onRestoreInstanceState
and onSaveInstanceState
. Scala made this really simple; case classes are serialisable so one simply calls Bundle.[get|put]Serializable
. Example:
val measure = savedInstanceState .getSerializable(BUNDLE_KEY_MEASURE).asInstanceOf[Measure]
Too easy.
1.3. Integrating with the Android Backup Manager.
My app is stateless so I just flipped the allowBackup
switch off in the AndroidManifest
and didn't have the need to write any code here, but it is something to be aware of. It's easy to forget but very important for stateful apps.
1.4. Supporting multiple screen densities.
Different phones have different screen densities and you should at least be targeting support for MDPI through to XHDPI. Different screen densities (loosely) require different copies of your graphics at different sizes. This wasn't a problem for me because I create all my graphics in SVG and have a script to convert them to PNGs using ImageMagick. (See the Artwork section for such a script.)
1.5. Providing landscape layouts.
Not mandatory but I've seen quite a few reviews on the marketplace where people get quite passionate (read: vehement) about not being able to turn their phone on its side. Personally I don't see why it's such a big deal but I'm not everyone. Creating a new layout for landscape-mode and having it coexist is easy. However, designing the landscape UI that is pleasing and effective is the time-consuming part.
Tip #1: Always use something like Inkscape to create the screen before you hit the code. Use real sizes and colours. Don't assume and leave out anything that you plan to do in code later.
Tip #2: Commit to doing this to all screens in your app up-front, or no none at all. Or do some. But make that decision early on and ensure you can make it work. You don't want to create 8 landscape layouts then later come across a few that won't work then decide to scrap the entire landscape orientation ability.
1.6. Ensuring layouts look acceptable in various conditions.
This means compiling a testing matrix of relevant attributes such as:
- Screen size.
- Screen density.
- Screen orientation.
- Android version.
- Ads vs no-ads (they do take up valuable screen space).
- Language.
Screen density I've addressed above. That, language and Android API version can just be eyeballed manually I think. No need to give them their own axis in a matrix. But I did want a matrix of ads-or-not, orientation, and screen size. What was annoying is that screen size is not screen size. There are resource qualifiers for screen size, namely small
, normal
, large
, xlarge
. Now they didn't work as expected for me. Due to my immense frustration and exhaustion at the time, I don't remember the details but I found them not aligning with my expectations at all. Instead I decided to use screen density to create different settings for different screen sizes, because there is a strong correlation between density and screen size anyway. I mean, no one has a 1200x800 LDPI phone. It doesn't exist. Thus I used a test matrix like this:
ads | no ads | |||
---|---|---|---|---|
port | land | port | land | |
ldpi | ✓ | ✓ | ✓ | ✓ |
mdpi | ✓ | ✓ | ✓ | ✓ |
hdpi | ✓ | ✓ | ✓ | ✓ |
xdpi | ✓ | ✓ | ✓ | ✓ |
Tip #1: Create a test matrix and use it to ensure things look good.
Tip #2: Eyeball the smallest screen sizes once in each language. Button widths can be twice as long as English depending on the target language which may cause side-effects with your layouts.
Tip #3: Always start with the lowest density and work your way up. A MDPI screen will choose values-ldpi/
over values/
if it exists and values-mdpi/
doesn't.
Tip #4: Don't trust the screen heights you can choose in the ADT layout editor. I have an old HTC Magic that has around 100dp less height than the closest config available from the editor.
Tip #5: Do not leave any hardcoded dimensions in your layout XML (obvious) or your styles.xml
(not obvious). When adjusting for each screen it's going to be the dimensions like the text sizes, the padding & margin sizes that you'll be adjusting.
Tip #6: Do your utmost, do your friend's utmost (!) to avoid using dimensions to affect the layout itself. For example, a dimension for the margin between elements is one thing but if you want a large river of negative space, don't use dimensions, anchor both sizes of the space to stuff so that the river size is organic. This makes for more consistent layouts on different devices and means you won't have to adjust the value for different screens.
2. Scala.
Learn it! Use it! It is a brilliant language. It allows you to write better code, more concisely. There is so much boilerplate bullshit that Java requires, so many hoops to jump through, and so many obstacles that prevent you writing the kind of code that you (well, I) want to. Scala does away with nearly all of those! Its strong focus on immutability, functions, maximum code reuse (yay for multiple inheritance again!) and conciseness, not to mention all the other modern goodness such as pattern matching/partial functions; for comprehensions; case classes; implicit conversions, classes, and parameters... The list goes on. It's very productive language and you will be more productive with it.
Like anything it does have it's shortcomings though: flaky IDE support, slow compilation time, WTF-inducing stacktraces, controversial lack of binary compatibility between major versions (which doesn't bother me at all actually), massive stdlib that Android can't handle without proguard, too many custom operators at times (look at SBT with its <+= <<= ++= <++=
etc, good god!).
But all in all, Scala is worth it. IDE support will continue to improve and compilation time and stdlib modularisation should be fixed later this year.
Scala can especially be a godsend when it comes to cutting out Android/Java repetition. Consider this Java:
protected TextView nameView;
protected void onCreate(Bundle savedInstanceState) {
...
nameView = (TextView) findViewById(R.id.name);
}
And now consider this Scala:
lazy val nameView = find[TextView](R.id.name)
Here's another example. Java:
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
onDelete();
}
});
Verses Scala:
view.setOnClickListener( onDelete _ )
Scala also saved me time on the testing front. Your test code becomes amazing when you can use traits for multiple inheritance, and write methods that accept lambdas or Ruby-style blocks. You'll end up with something approaching a testing DSL before you know it, and most of your test cases will be under 3 lines which certainly taking the wind of the sails of any test-case-writing-laziness.
Tip #1: Use an incremental compiler for Scala. With the scala-maven-plugin this is a single config item.
Tip #2: Copy a proguard.cfg with Scala settings and set your build to always use proguard.
Tip #3: Use traits to create collections of test helper code. Do as little directly as possible in your immediate test case code.
Tip #4: Use implicit conversions for all things Android.
Tip #5: Create an ActivityHelperTrait or similar and load it up with implicit value classes and inlined helper methods. Example:
@inline def find[T <: View](resId: Int): T = findViewById(resId).asInstanceOf[T]
3. Artwork.
Get in the habit of using vector-based graphics and using ImageMagick's convert
tool (it's command-line). Vector graphics look great at any size and you can convert
to generate bitmaps at various sizes (which Android needs). If you want you can use SVGs directly in Android with svg-android but there are still places where you need fixed-size images.
You will need to create or procure a visual to use as your application icon before release. Ensure you have it as an SVG (or some other vector-based format) then use ImageMagick to create various sizes. The marketplace requires 512x512, where as your app requires 36x36, 48x48, 72x72, 92x92 for ldpi, mdpi, hdpi and xhdpi respectivelly.
For all kinds of visuals you'll need to have multiple copies in separate sizes to support different screen densities. I wrote a Ruby script to convert all my visuals. Here is an except of that script:
Tip #1: Use 32-bit PNGs if you want an alpha channel.
Tip #2: ImageMagick will be your best friend for conversion. Script it.
Tip #3: Inkscape is a great tool. Very easy to use and everything is vector-based.
Tip #4: There are plenty of good-looking, vector-based visuals out there that are public-domain. There are also sites like The Noun Project that have brilliant visuals that you can usually use free as long as you attribute the author, or can buy cheaply.
I'm one of those people vehement about creating an app that works in landscape. Or, at the bare minimum, *not locking it to portrait*!
ReplyDeleteReason being that there are devices out there whose default orientation is landscape. There are a bunch of such phones, but I bought a Samsung Galaxy Y Pro specifically for testing this stuff.
Oh really? Mate, thank you for that information! I wasn't aware of that. That explains the frustration I've seen in some app review comments. Hmm, I'll have to spend some time looking into and thinking about that.
DeleteGREAT article! I love the structure and your to-the-point style of writing in this post. A lot of tips to pick up on for everybody here, including experienced developers.
ReplyDeleteA have a couple of questions regarding 1.6 though. Tip #5 please explain why it's obvious not to have "hardcoded" measures in the XML. Do you include Density Pixels ("dp") in your definition of "hardcoded"? Since Android picks XML files based on screen size and resolution as you obviously are aware of, I don't see why it's a problem, and in fact the official documentation is scattered with examples of this.
Second, how did you implement the ads vs no-ads layout files in practice? Did you have separate XML files for the no-ads version, and if so - how did you make the Android framework load them instead of the default files for the set resolution?
Hi there Frode, thanks for the positive feedback :)
DeleteRe: hardcoded measurements, I may have been unclear. I'm not referring to the measurement unit, the developer should choose between dp/sp/px etc as appropriate for their needs and the situation; rather I was referring to storing the numbers themselves in the layout XML. Say you have an attribute such as android:textSize="20sp" in your layout XML, if you're trying to save space on a small or low-density screen you're probably going to want to change the 20sp to a 18sp. If the 20sp is in the layout XML you'd have to copy & paste the entire layout to one with a different qualifier which is bad because it's more to maintain and will probably go out of sync. In nearly all cases your layout itself will be fine, it's just the numbers/meaurements you'll want to change, thus, it's best not to put the 20sp in the layout XML (which is what I meant by hardcoded), but rather put it in a dimensions XML, give it a name, reference it in the layout XML (ie. android:textSize="@dimen/size"). That way you can modify just that number in isolation, the layout remains shared.
For the ads vs no-ads, I did this.
1. Create a library, free, paid projects. The free & paid projects will inherit the library one.
1. Create layout XMLs normally in the library project and use to use ads.
2. Create an empty ads layout XML in the library. It will include just a and the visibility will be "gone".
3. In the paid app, create an ads XML with the same name and use a proper AdView.
Have a look at template #3 on my other post.
http://japgolly.blogspot.com.au/2013/02/android-project-templates.html
Hope that helps!
Thanks that cleared things up :) I'm developing an ad/no-ad app myself so I'll be using that trick as well!
DeleteDavid Barri,
ReplyDeleteDo you have a step by step tutorial in how to confifure an Scala environment to develop Android application?
Thanks
Hey. Nah, not exactly but I did make some template projects with Scala + Android that you can look at or prune down to what you need.
Deletehttps://github.com/japgolly/reference/tree/master/android-scala-maven-single