Set Up
I'm using Android Studio to do the development. At home, where I'm doing this development, I like to run the Canary branch. I've run Canary at work before; and it's helped in a number of situations to know what's coming. Right now; that future sight isn't as useful as being on the same configuration as everyone else to be able to provide exact support when requested.
New Project
Doing the new work; we'll start a new project
While it can be feature limiting; we're going for a basic app, so until we need new shiny stuff; we'll go back to API 16
The last bit I like to do when starting a project is to have an empty activity. It sets up all the folders for later development, and is easy enough to clean up.
Creating this does it's magic and we have an app! Not doing much; but an app!
Git
I prefer Git as the source control. This is going to live in GitHub, so Git's a pretty good choice.
.gitIgnore
One of the important things for Git is a well configured .gitIgnore
file. I've been using a standard across projects for a while now. I developed it months ago, and occasionally update it for new things, but in general this is what I like to have in my Android Studio based projects.
Read Me
I like to have a ReadMe.md even if I don't do much with it. This is where I can add links/references to scripts or tools to get the project running. Create this, give a small blurb and call it good.
Init Git
Setting up the AndroidStudio project doesn't do anything with Git for us, we need to initialize this ourself.
While I know the commands to do all of this locally, we'll need the repo created in GitHub. We can't add any files to the repo or there will be history conflicts trying to push. Which is why we add the .gitIgnore and ReadMe.md locally, and ignore the license. Which I normally do as MIT; and will just pull in a file from another project.
Because we haven't added any files in GitHub; our repository prompts us to init one locally
We'll do a slightly modified version of the new repo locally via the command line
git init
git add .
git commit -m "initial commit"
git remote add origin git@github.com:Fyzxs/HackerNewsReader.git
git push -u origin master
Implementation
We've got an app shell, tweaked the ReadMe, set up a .gitIgnore and LICENSE file, then pushed everything to a repo! We're looking good to start building our Retrofit Layer!
Packaging
There are only 2 hard problems in computer science; Cache, Naming, and off by 1 errors.
Where do you place things? How do you organize your code files? There's never a good answer to this. There's preferences and situational advantages, but I've never seen "a correct" way. I'll be doing the package structure in a way I've found to work well; and maybe I'll evolve it over time. When I create a new package I'll try to explain why.
Which leads us to our first package. Android Studio created the com.quantityandconversion.hackernews
package for us; and stuffed our default MainActivity.java
in there. That's fine, I'll be dealing with the UI in subsequent posts.
Here we are looking at where to place Retrofit related code. I ask myself, "What is retrofit?" and the answer is, "Network". I normally place it in a 'top' level network
package. Well... normally it doesn't take me 3 tries to get it right... Like I said, naming is hard....
I've brought up Retrofit a few times here; and haven't yet linked out to it
=>> RETROFIT <<=
If you're not familiar with Jake Wharton's work; might not be a bad idea to see what kinda stuff he's worked on. A lot of utility projects; which Retrofit is one of.
Retrofit simplifies making and handling HTTP calls. I've found it to be fantastic; and am happily using it.
Gradle
We need to pull retrofit into our project; I find the best mechanism is via Gradle's compile
.
You want to do it differently, go for it; I'm using
compile 'com.squareup.retrofit2:retrofit:2.1.0'
I also need to clean out the gradle file. It includes a bunch of defaults that I dislike.
Pre-Fix w/oRetrofit
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.1.0'
testCompile 'junit:junit:4.12'
}
Post-Fix w/Retrofit
dependencies {
//Android
compile 'com.android.support:appcompat-v7:25.1.0'
//Retrofit
compile 'com.squareup.retrofit2:retrofit:2.1.0'
//Local Testing
testCompile 'junit:junit:4.12'
}
Gradle is now configured and ready for us to start.
Api Interface
This post isn't an explicit tutorial on how to use Retrofit; it's an example using Retrofit. The documentation on the Retrofit website is pretty good; it's always my starting point.
Now it's time to create the API for our network calls!
Set up
Actually this will just implement a single rest call for the API. This is about setting up the foundation and have an example of how to move forward with other calls.
As we want to display the top stories; we'll start with a Top Stories. Simple enough.
What's the first step? Create the interface? I always want to start there; but for emphasis on the TDD approach; I'm going to do very small steps; and very close to "TDD like you mean it!".
This means that the first step is to create the test class. I won't do the super in depth aspect of pure TDD in the write up; but I am going to be practicing it (as best I can) while I go through this.
I've gotten a quick method TDD'd and the interface w/topStories
method exists.
This does presuppose the return type of the data. While I'll approach TDD from the very pure perspective; when trying to get things done, bigger steps are fine. It's the Obvious Implementation
While I could TDD annotations and drive their existence via that as well; It's an implementation detail and would be a pain for little gain; so I'll just do the annotations w/o test support; I know, I know.
This is the core of defining an API for use via retrofit. There's a bit of a build that's coming next; but an interface and methods with annotations is what we focus on.
I renamed the interface from HackerNewsService
to HackerNewsApi
. This will fit better with some additional layers I apply to my utilization of retrofit. There's some other changes I also like to do. One of these is due to JAVA's protection mechanism. The lack of a "internal" modifier, like C#, forces either huge packages to use the package-local scoping, or things public and "you shouldn't use this" warnings.
I'm going to go for the "don't use this" style. This is an app, so this style works well. For an library project I definitely use the giant package form of hiding things.
I indicate that visible classes shouldn't be used by the inclusion of an internal
package. It's not a huge thing, but it's something that will hopefully stand out.
Enough restructuring; let's get some Retrofit action going on! ... errr....
Can't have enough restructuring... I renamed a few more things... It happens. We'll see if you can figure it out as I'm not re-writing this. It's part of the process; merciless refactoring.
Retrofit requires a Builder
that consumes the interface to produce the calls for us. Having used Retrofit before and understanding how the builder works together with the convertors (see site for json->object conversion tools).
I call the layer producing this the Network
. Normally it's a single class for each API interface.
We'll need to TDD this into existence. It's going to encapsulate the Builder construction, so calls into the ItemNetwork
class should be fairly 1:1 with the API. We'll start by assuming that's the case.
We can get a simple failing Network
test by asserting we're not getting null.
From here, we can do a convoluted process to get to the Retrofit Builder'; or we jump to the obvious implementation and save us all a lot of pain.
One of the components we need to test the networking layer using Retrofit is the OKHttp MockWebServer.
This requires a couple additions to our app's gradle file. The MockWebServer import requires that the app be using the same version of okhttp. While we could match it to whatever Retrofit imports; we can also specify the version of OKHttp we want to use; and we will.
compile 'com.squareup.okhttp3:okhttp:3.5.0'
testCompile 'com.squareup.okhttp3:mockwebserver:3.5.0'
I won't go into detail about utilizing the MockWebServer
; check out the github on it; or the examples in code.
Because we're trying to do a little 'more'. Such as the parse expected data into objects; we need to pull in the JSON parser; I like MOSHI. It's small and straight forward, works well for me.
The update to gradle is
compile 'com.squareup.retrofit2:converter-moshi:2.1.0'
This allows us to build adapters to easily convert the json into classes through constructors.
All of this has been to get a test passing where calling topStories
did not return a null object.
It doesn't - The tests pass!
YAY!!!
OK, we're not done yet; we haven't adapted the data into an actual object yet. Which shouldn't be particularly hard since they're just longs.
The final URL we hit is https://hacker-news.firebaseio.com/v0/topstories.json
which should just be a list of IDs into the Items collection.
The next step is to actually make the fake network call.
This is a modification to the makeTopStoriesRequest
to perform the network call synchronously. We will want it async, but this is a simpler implementation to do it sync. I know how to do it async just fine... and the fact there's a lot of red starting at me trying it sync concerns me.
Ahh, I had it as an element "{}"
, not an array "[]"
in the test.
Passes, so the Adapter doesn't blow up.
Could be chance since we don't have any data, let's throw a number into the array and the ability to get size from the Items
; which has required an ItemIdAdapter
.
Sync correctly parses the values. While I'd like to do the async... at this point; that 'upgrade' in the test is testing the framework. We've confirmed all of our code works; there's no need to complicate with the async behavior. ... yet.
Summary
I think this post covers the set up of Retrofit pretty well. It is very basic, and doesn't do a lot; but sometimes it helps to have the simplicity in place.
All the code up to this point is available for review here which is tied to the hash; so always the code at the end of this post. The 8 commits at this point show working through this post.