Skip to content

Instantly share code, notes, and snippets.

@maximeroussy
Last active October 19, 2018 15:34
Show Gist options
  • Save maximeroussy/26eb36ccc4f93dbe48438b42b61c9e8b to your computer and use it in GitHub Desktop.
Save maximeroussy/26eb36ccc4f93dbe48438b42b61c9e8b to your computer and use it in GitHub Desktop.
R&P | Android Code Challenge
asbtract class ApiModel {}
@Module
class ApiModule {
@Singleton
@Provides
fun okHttpClient(fileManager: FileManager): OkHttpClient {
val okHttpClient = OkHttpClient.Builder()
val cacheSize = 10 * 1024 * 1024 // 10 MB
val cache = Cache(fileManager.getCacheDirectory(), cacheSize.toLong())
okHttpClient.cache(cache)
if (BuildConfig.DEBUG) {
val logging = HttpLoggingInterceptor()
logging.level = Level.BODY
okHttpClient.addInterceptor(logging)
}
return okHttpClient.build()
}
@Provides
fun restEndpoint(okHttpClient: OkHttpClient): RestEndpoint {
val baseUrl = "https://api.APIHOST.com/"
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
.create(RestEndpoint::class.java)
}
}
data class Comment(
val id: Long,
val content: String
)
data class CommentApiModel(
@SerializedName("id") val id: Long,
@SerializedName("content") val content: String
) : ApiModel()
class CommentApiModelMapper @Inject constructor() {
fun map(comment: Comment): CommentApiModel {
return CommentApiModel(comment.id, comment.content)
}
}
class CommentRepository @Inject constructor(
private val restApi: RestApi,
private val commentApiModelMapper: CommentApiModelMapper
) : Repository<Comment> {
...
override fun save(comment: Comment) {
restApi.save(commentApiModelMapper.map(item))
}
...
}
interface Repository<T> {
...
fun save(item: T)
...
}
class RestApi @Inject constructor(restEndpoint: RestEndpoint) {
...
fun save(item: ApiModel) {
when (item) {
is CommentApiModel -> {
val result = restEndpoint.saveComment(item).execute()
if (!result.isSuccessful) { throw Exception("Error POSTing comment to api.") }
}
else -> throw IllegalArgumentException("Argument model is of unrecognized type.")
}
}
...
}
interface RestEndpoint {
...
@POST("/comments/new")
fun saveComment(@Body comment: CommentApiModel): Call<ResponseBody>
...
}
class ScreenViewModel(private val repository: Repository<Comment>) : ViewModel() {
...
fun saveComment(comment: Comment) {
launch {
// launch repository operation on background thread
try {
repository.save(comment)
withContext(UI) { showSuccessSaving() }
} catch(e: Exception) {
withContext(UI) { showError(e) }
}
}
}
...
}

Assumptions

  • Assume the remote data store is a generic REST API.
  • Assume the save comment endpoint call doesn't have a response object
  • Assume all objects (Comment, Annotation, etc.) will follow identical basic CRUD operations (SAVE, GET, DELETE, UPDATE)

Design & Architecture

I decided to implement a version of "clean" architecture with layers of abstraction. You can imagine four layers:

  • Presentation: Contains views and view logic (activities, fragments, viewmodels, presenters, etc.) such as ScreenViewModel.
  • Domain: Contains core object models such as Comment as well as the core data interface Repository and any core business logic. *In this particular case, since we're assuming basic CRUD operations, there isn't really any "business logic" to speak of, so we have the presentation calling the data repository directly instead of through usecases/interactors.
  • Data: Contains implementations of the core data interface such as CommentRepository as well as mappers such as CommentApiModelMapper and serves as an abstraction to lower data sources.
  • Api: Contains remote data source api classes such as RestApi, RestEndpoint and api models such as CommentApiModel and the ApiModel superclass.

Since this is Kotlin pseudocode and not a full blown project, I've omited several irrelevant files. That being said, you can probably notice hints of some of the dependencies that might be included in this project based on some annotations and classes. Here's an assumed list to make it clear:

  • Dagger2
  • OkHttp
  • Retrofit
  • Gson
  • Kotlin coroutines
  • Architecture Components (ViewModel)

Testability

The project is built with testability in mind with dependency injection for all classes. Some of the Dagger configuration files weren't included for simplicity, but I've thrown in a few modules to make a few dependencies more clear. I usually favor constructor injection whenever I can and default to modules for interface implementations, builders/factories, or "Strategy Pattern" type runtime injection.

Separation of Concerns (SoC)

By abstracting the project with several layers that have distinct, minimal responsibilities, we gain a lot of flexibility with refactoring and evolving the code. We could rewrite the whole api layer with a different remote host and the rest of the code such as the domain, presentation and even some of the data/repository layer wouldn't have to change. We could introduce a new layer for a local data store to persist data when offline and the only thing that would have to change is the repository implementation.

Each layer is unaware of the implementation details of the layer below.

Don't repeat yourself (DRY)

*One thing to note is that Kotlin itself removes a whole lot of boilerplate code and helps with reducing duplication. It also offers lots of interesting tools like extension functions and sealed classes.

For this solution, I've implemented two different approaches to reduce code duplication and favor reusability.

On the api side of things, I used inheritance to generalize the RestApi class to accept any ApiModel. The RestApi then uses Kotlin's when() {} to call the correct endpoint. This way, adding a new data class to the project would simply require adding a new implementation of ApiModel that matches the server's response model and can be serialized properly, and adding an entry to the RestApi's when {} operator.

On the data/repository side of things, I used generics to create a single data repository interface Repository that can handle any and all data types such as Comment. This approach still has a bit more code duplication since we need to write a new Repository implementation for every new data class. However, it doesn't require us to perform type checks or casting.

I could've implemented the data/repository layer with a similar inheritance approach to try to reduce code duplication further but I felt like that might clutter the domain layer and its models too much.

Conclusion

We can always push abstraction and DRY principles further but it becomes a question of balancing the pros and cons. In many cases, a bit of repetition can help keep the code clear and readable, which is a huge deal.

@Module
class ViewModelModule() {
@Provides
fun screenViewModel(commentRepository: CommentRepository) = ScreenViewModel(commentRepository)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment