Muselee is a demo app which allows the user to browse popular music artists. It is not intended to be a fully-featured user app, but a vehicle to explore good app architecture, how to implement current best-practice, and explore how the two often go hand in hand. Moreover it will be used to explore how implementing some specific patterns can help to keep our app both maintainable, and easy to extend.
In the last article we looked at the app
module and saw how minimalistic it is – it is essentially just the application level components., which makes a certain amount of sense! Next we’ll look at the core
module which, as we have already discussed, needs to be kept fairly lean as well. We need to be disciplined in including only those components that really are common to all of the others. One example of this is downloadable font definitions. Downloadable fonts may be used by many components and are likely to be a part of an overall design style for the entire app. Also they are unlikely to change that often, so they are a good candidate for inclusion within the core
module. They could not go in the app
module because then they would not be available to the feature modules which do not have a dependency on the app
module. I won’t bother showing these files here because they are fairly standard stuff – they are in the res folder of the app source if you want to take a look at them.
The next thing that we’re going to include in the core
module is a utility class which will perform connectivity checking.. We’ll keep this really simple for now – we just want to know whether or not we have an active network connection, and we don’t require any logic which may depend upon the quality of the connection:
class ConnectivityChecker @Inject constructor(private val connectivityManager: ConnectivityManager) { val isConnected: Boolean get() = connectivityManager.activeNetworkInfo?.isConnected ?: false }
This has a dependency upon ConnectivityManager
, but this is already provided by the app
module in the ApplicationModule
which we looked at in the last article. This shows some of the versatility that dependency injection gives us. Although the core
module does not have a dependency upon the app
module, it can still use components which the app
module adds to the dependency graph.
Next we’ll take a look at the DI for some basic network functionality:
@Module object CoreNetworkModule { @Provides @JvmStatic internal fun providesLoggingInterceptor(): HttpLoggingInterceptor? = if (BuildConfig.DEBUG) { HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } } else null @Provides @JvmStatic internal fun providesOkHttpClientBuilder( loggingInterceptor: HttpLoggingInterceptor? ): OkHttpClient.Builder = OkHttpClient.Builder() .apply { loggingInterceptor?.also { addInterceptor(it) } } }
Many feature modules will require an OkHttpClient
instance upon which to build network calls. By providing these from the core
module we are able to apply consistent logging behaviour throughout the app. We’ll see some further examples of how this approach can benefit us later in the series.
The final major piece of functionality that we’ll add to the core
module is the ability to instantiate Jetpack ViewModel
instances dynamically. This requires a little bit of infrastructure:
@Singleton class ViewModelFactory @Inject constructor( private val viewModelProviders: Map, @JvmSuppressWildcards Provider > ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class ): T { val provider = viewModelProviders[modelClass] ?: viewModelProviders.entries.first { modelClass.isAssignableFrom(it.key) }.value return provider.get() as T } }
This is the ViewModelFactory
that we looked at in this article, so I won’t give a fully detailed explanation here. it provides us with a mechanism for obtaining ViewModel
implementations of specific types. This is made available as a ViewModelProvider.Factory
instance which is available to the AndroidInjector
component:
@Module abstract class BaseViewModule { @Singleton @Binds abstract fun bindViewModelFactory(factory: ViewModelFactory) : ViewModelProvider.Factory }
So this allows us to provide specific ViewModel implementations to the AndroidInjector, but we still require a mechanism to actually provide these specific ViewModel
implementations. We do this through an annotation:
@Target( AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER ) @Retention(AnnotationRetention.RUNTIME) @MapKey annotation class ViewModelKey(val value: KClass)
We don’t actually define any ViewModel implementations within the core
module so we can see how this all works together, but we’ll look at that in the next article.
Next we’ll add is a really simple interface which will come in really handy later on:
interface DataProvider{ fun requestData(callback: (items: List ) -> Unit) }
This requests data of a specific type, and passes it on to a callback function. We’ll explore the full scope of what this seemingly very simple interface offers us later on in the series.
Finally we’ll add another interface which will come in handy later on:
interface DataMapper{ fun map(source: S): R }
This interface defines a behaviour for mapping an object of one type in to another.
So while we have a project that compiles, it still really doesn’t do very much. However that will change in the next article where we’ll add out first feature module which will display a list of the top artists obtained from the last.fm API.
The source code for this article is available here.
© 2019, Mark Allison. All rights reserved.
Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.