A set of hopefully useful classes for common networking use cases.
The stanwood Network Utilities are hosted on JitPack. Therefore you can simply import them by adding
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
to your project's build.gradle
.
Then add this to you app's build.gradle
:
dependencies {
implementation 'com.github.stanwood.framework-network-android:arch:<insert latest version here>' // aar version available as well
implementation 'com.github.stanwood.framework-network-android:cache:<insert latest version here>' // aar version available as well
implementation 'com.github.stanwood.framework-network-android:core:<insert latest version here>' // aar version available as well, automatically included when pulling in one of the other libraries
}
Refer to the extensive javadoc of the provided classes for details on how to use them. Right now there are solutions for the following use cases:
- handle offline situations and caching when using OkHttp (
cache
library) - generic token based authentication handling with OkHttp (
auth
library) - generic network related utility classes (some specific to apps developed over here at stanwood) (
core
library)
See javadoc of the classes in the library.
The auth
library contains classes for handling token based authentication with OkHttp. Generally this is done via Authenticators and Interceptors.
If you are using plain username/password authentication you obviously won't need the following classes as there will never be a need to refresh the data and authentication is straightforward.
Integration into both existing means of token retrieval as well as from scratch is simple.
First you need to implement both TokenReaderWriter
and AuthenticationProvider
. The first is used for reading (to find out whether we are still using an old token) and writing (for authentication) tokens from/to requests. The second provides means to get authentication data (such as tokens and sign-in status) and should also be used by you directly during initial authentication (i.e. right after user log-in/registration) for consistency reasons. The AuthenticationProvider
usually makes use of an AuthManager
to store retrieved login data. You can get a sample implementation of such an AuthManager
by using the stanwood Android Studio Plugin (see AuthManager section further below). Refer to the javadoc of the aforementioned classes for more details on how to implement these interfaces.
In the future optional modules will provide implementations for common use cases.
Then create an instance of io.stanwood.framework.network.auth.Authenticator
:
Authenticator authenticator = new Authenticator(
authenticationProvider,
tokenReaderWriter,
null
);
And an instance of AuthInterceptor
:
AuthInterceptor authInterceptor = new AuthInterceptor(
new ConnectionState(appContext),
authenticationProvider,
tokenReaderWriter,
null
);
Construct an OkHttpClient
and pass the interceptor and the authenticator:
new OkHttpClient.Builder()
.authenticator(authenticator) // takes care of 401 (invalid token, get fresh one and retry)
.addInterceptor(authInterceptor) // adds authentication data to every request
.build();
That's it. When using this OkHttpClient
instance you'll benefit from fully transparent token handling.
Hint 1: If your app uses multiple authentication methods make sure to
implement an own subclass of AuthenticationProvider
for each method and use
an own OkHttpClient
for each!
Hint 2: In case you are using OkHttp/Retrofit to retrieve tokens: As the
Authenticator
locks all other Authenticators
and AuthInterceptors
with
the same AuthenticationProvider
it makes sense to provide an own
OkHttpClient
instance (and thus also an own Retrofit instance if you use it)
for all calls you need to receive new tokens. If try to serve all calls with
the same client instance, it may happen that you run out of connections/threads
while trying to get a token because there might be a whole slew of requests
already waiting for that token call to succeed. Alternatively you can also try
to increase the executor pool size, but this is not recommended as you never
know for sure how many requests are executed at a given point in time - and you
definitely always want a thread ready for your token request.
Hint 3: Rules of thumb (exceptions apply):
- one
AuthenticationProvider
per authentication method (however you can also try to wrap multiple ones in oneAuthenticationProvider
which can ease DI quite a bit - we recommend to define each method in one provider and then pass those providers to the wrapper provider to have clean separate implementations) - one
TokenReaderWriter
andAuthManager
per Api
The library provides an own exception class called AuthenticationException
.
You can listen for this class in your Retrofit
Callback.onFailure(Call, Throwable)
to check for authentication related
errors.
If you're having problems retrieving a token in the AuthenticationProvider
(e.g. due to an invalid refresh token) always try to resolve those issues there
as well if possible. Throwing an AuthenticationException
should just be a
last resort.
Alternatively you can also subclass the Authenticator
class and override
onAuthenticationFailed()
if you want to trigger special handling for failed
authentication from which we couldn't recover with our default handling. This
usually tends to be a bit harder to get right as it expects you to modify the
failed request directly without any means of intercepting the response. Thus
handling token retrieval issues should preferably handled in the
AuthenticationProvider
as explained above.
The stanwood Android Plugin provides a template for an AuthManager
which takes care of storing and providing authentication info like tokens and usernames. It will automatically be generated for you when running the NetworkModule assistant in the New -> Stanwood menu.
You can extend it with authentication types for all your authentication providers for easy and streamlined authentication state handling.
The util
package contains generic network related classes. The util.stanwood
package contains classes for apps developed over here at stanwood.
You can find an example over here (private for now).
Add the following dependencies (replace versions here with current versions):
def retrofit_version = '2.5.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.okhttp3:logging-interceptor:3.13.1'
Write an interface class with Retrofit API definition and put it in the datasources.net.<api>
package:
interface GithubApi {
@Headers(
"Accept: application/vnd.github.mercy-preview+json"
)
@GET("search/repositories")
fun getMostPopularAndroidRepositories(
@Query("q") query: String = "topic:Android",
@Query("sort") sort: String = "stars",
@Query("order") order: String = "desc"
): Single<SearchRepositoriesResponse> // this is for RX
}
To define models classes use curl or postman or insomnia to execute the request and get a sample response. Then use the JSON to Kotlin Class Android Studio plugin and paste the response there. We use GSON for de-/serialization of JSON so use the proper annotation settings in the plugin. Configure the plugin so that it uses val
s and try the Auto determine Nullable or not from JSON Value setting (make sure to revisit the generated classes later to check whether the properties are fine). Additionally switch on Enable Map Type when JSON field key is primitive type in other settings.
Put the generated classes in the datasources.net.<api>.model
package.
Then define a NetworkModule
similar to the following one:
@Module
class NetworkModule {
@Provides
@Singleton
fun provideGson(): Gson = GsonBuilder()
.setDateFormat("YYYY-MM-DDTHH:MM:SSZ")
.create()
@Provides
fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor =
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
}
@Provides
fun provideGithubOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor,
application: Application
): OkHttpClient {
val client = OkHttpClient.Builder()
.readTimeout(TIMEOUT_MS, TimeUnit.MILLISECONDS)
.connectTimeout(TIMEOUT_MS, TimeUnit.MILLISECONDS)
.cache(Cache(application.cacheDir, CACHE_SIZE))
.addInterceptor(TestfairyHttpInterceptor()) // only if you use the stanwood analytics library
.addInterceptor(StanwoodHeaderInterceptor(
// make sure that app name is language independent, maybe use a flavor or a static BuildConfig String
application.getString(R.string.app_name),
BuildConfig.VERSION_NAME,
BuildConfig.BUILD_TYPE
)
if (BuildConfig.DEBUG) {
client.addInterceptor(httpLoggingInterceptor)
}
return client.build()
}
@Provides
@Singleton
fun provideGithubRetrofit(
okHttpClient: OkHttpClient,
gson: Gson
): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.GITHUB_API)
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
@Provides
fun provideGithubApi(retrofit: Retrofit): GithubApi = retrofit.create(GithubApi::class.java)
}
Add the NetworkModule
as an include to your AppModule
and you can start injecting your API class to perform network requests.
This project follows the Android Kotlin Code Style for all Kotlin classes. We use ktlint for automated checks.
The ktlint-gradle-plugin is already integrated into this project which means you need nearly no setup on your end.
Just execute those two calls to configure your IDE for this project and add a pre-commit hook to do the checking (and if necessary formatting for you).
./gradlew ktlintApplyToIdea
./gradlew addKtlintFormatGitPreCommitHook
Make sure to check the Run Git hooks checkbox if you commit via Android Studio (should be set by default).
Last thing to do is setting line length to 140 (Preferences -> Editor -> Code Style) - unfortunately ktlintApplyToIdea
can't do that for you.
The CI will check formatting as well and won't allow non style conform commits to be merged.