Skip to content

An Android app that shows a list of Users and related photos. It's a sample coding challenge. (100% Kotlin)

Notifications You must be signed in to change notification settings

ravimhzn/SampleTestAndroidApp

Repository files navigation

SampleTestAndroidApp

An Android app that shows a list of Users and related photos. It's a sample coding challenge. (100% Kotlin)

This app has modified gradle.properties file which helps organize gradle much effectively.

Screenshots

Project Used:

  • Android Studio 3.6.3
  • Kotlin Version 1.3.72
  • Gradle Build Version 3.5.3
  • Android Version 10
  • JVM 1.8

Architecture

MVI (MVVM + more...) + Architecture Components (ViewModel & Lifecycle-aware components) + Dagger2 + LiveData + Kotlin Coroutines + Retrofit

I used MVI architecture which is basically a MVVM with better state management and shared Viewmodels, StateEvent and ViewState.

MVI is short for Model-View-Intent. Here, Intent means the intention with which we're performing certain actions in our app; not Intent, as in the Android object.

Abiding the Sigle Source of truth principle, Repository is designed accordingly which can easily be modified in future if we need to do caching in future. I've tried and experimented to generify as much classes as possible on this project, mainly, Network and Repository classes to handle DataState, StateEvent and ViewState effectively.

Use Cases are used to perform business logic operations on the data obtained from the repositories.

Why MVI?

Great question!

MVVM (Model-View-ViewModel) is better and recommended design-pattern by google in the sense that it doesn't require you to have a View for each ViewModel, but there is the problem of state synchronization. It gets difficult and weird sometimes with ViewModel and it tends to be out of sync if the code isn't robust.

So how does MVI solve this issue?

That's another fantastic question, you're on quite a roll!

MVI solves the state synchronization issue by storing states as model classes, in the form of Kotlin sealed classes. Model as State: UI might have different states - Loading State, Data State, Error State etc. In MVI, models are formalized as the container of states which is not the case with other patterns. Every time a new immutable model is created which is then observed by the view. This way of creating an immutable model will ensure thread safety.

To read more about MVI, why it's a good architecture and how it solves the state synchronization issue, you can find the article by Hannes Dorfmann here:
Model-View-Intent on Android

Sample STATEEVENT used on this project:

sealed class MainStateEvent :
    StateEvent {

    class GetUserListEvent() : MainStateEvent() {
        override fun errorInfo(): String {
            return "Unable to retrieve all user lists"
        }

        override fun toString(): String {
            return "GetUserListEvent"
        }
    }

    class GetPictureList(val id: Int) : MainStateEvent() {
        override fun errorInfo(): String {
            return "Unable to retrieve picture lists"
        }

        override fun toString(): String {
            return "GetPictureList"
        }
    }
}

Sample VIEWSTATE used on this project:

@Parcelize
data class MainViewState(

    var activeJobCounter: HashSet<String> = HashSet(),

    var fragmentUserList: FragmentUserList = FragmentUserList(),

    var fragmentPictureList: FragmentPictureList = FragmentPictureList()

) : Parcelable {

    @Parcelize
    data class FragmentUserList(
        var arrUserList: List<UserListResponse>? = null,
        var userListResponse: UserListResponse? = null,
        var layoutManagerState: Parcelable? = null
    ) : Parcelable

    @Parcelize
    data class FragmentPictureList(
        var arrAlbumListResponse: List<AlbumListResponse>? = null,
        var albumListResponse: AlbumListResponse? = null,
        var layoutManagerState: Parcelable? = null
    ) : Parcelable

}

Sample DATASTATE used on this project (as recommended by Google):

data class DataState<T>(
    var error: ErrorState? = null,
    var data: T? = null,
    var stateEvent: StateEvent
) {

    companion object {

        fun <T> error(
            errorMessage: String?,
            stateEvent: StateEvent
        ): DataState<T> {
            return DataState(
                error = ErrorState(
                    errorMessage ?: UNKNOWN_ERROR
                ),
                data = null,
                stateEvent = stateEvent
            )
        }

        fun <T> data(
            data: T? = null,
            stateEvent: StateEvent
        ): DataState<T> {
            return DataState(
                error = null,
                data = data,
                stateEvent = stateEvent
            )
        }
    }
}

Single Source Of Truth Principle

Each component might have its own state. View and Presenter/ViewModel are meant as the component here. Maintaining states separately at different levels might cause state conflicts. So to avoid these kinds of issues, the state is created at one level (Presenter/ViewModel) and passed to another level (View) in MVI. The only way to change the state is by firing an Interaction by the user. This kind of approach restricts any change to the State of the system only via a defined set of actions. An undefined action by a user cannot cause any undesired change to our System.

Gradle Configuration

I wanted to structure my dependencies much effectively. Multi-module Android projects are now the recommended way to take advantages of performance improvements with Android Gradle Plugin 3+. Hence I followed an article by Sam Edwards on Kotlin + buildSrc for Better Gradle Dependency Management. You can find all my gradle dependencies under buildSrc Directory.

Libraries Used

This app uses AndroidX libraries with modules like appcompat, core-ktx, constraintlayout, cardview, recyclerview and swiperefreshlayout.

Android Architecture Components like ViewModel and Lifecycle-aware components are implemented in this app for a more robust, testable and maintainable codebase.

Dependency injection (Dagger2 is easy to use library for managing dependencies, it is perfect for small/mid size projects_)

Retrofit is a de-facto standard nowadays. Retrofit with GSON is used for networking in the app to make API calls on this project.

Kotlin Coroutines is used for Reactive Programming throughout this app. It's a good replacement for RxJava and RxAndroid libraries. You'll find most of its usages in Repositories/ NetworkBoundResource, ViewModels and Use Cases. It also works with Retrofit to adapt API responses into Observables.

Picasso is the image library used in this app. It is fast and offers more fine-grained control over image loading. I've choosed Picasso over Glide. Glide had issues rendering provided imageUrl.

Material Components provide beautiful UI elements with a lots of customizations. I've used this because it defines the latest standards for design by Google.

JUnit is the unit testing framework that I've used in my app.

Mockito is the mocking framework for unit testing I've used in this app and it lets you create test objects for the purpose of test-driven development or behaviour-driven development.

To Run the Test:

gradlew.bat test --stacktrace (Unit Test For windows)
./gradlew test --stacktrace (Unit Test For windows)
gradlew.bat connectedAndroidTest --stacktrace (Instrumentation Test For windows)
./gradlew connectedAndroidTest --stacktrace (Instrumentation Test For windows)
//Note: You can find the HTML Test Report inside "\app\build\reports\tests\"

Things To Work On In The Future

  • Data Binding could be used for cleaner View-related code. I've yet to explore Data Binding more deeply which is the reason I've skipped it on this project currently.

Developed By

RAVI MAHARJAN

License

Copyright 2020 Ravi Maharjan

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Releases

No releases published

Packages

No packages published

Languages