Giter Club home page Giter Club logo

dazn-code-challenge's Introduction

DAZN Code Challenge - Android TV Gradle Build codecov

A responsive Android sample app written in Kotlin and Jetpack Compose, supporting different navigation layout on screen sizes. The Media 3 Exoplayer is implemented on top of the single activity architecture. It is fully functional with Picture-in-Picture support.

The events under the Events tab provides video playback. The schedule under the Schedule tab refreshes automatically every 30 seconds. For both tabs, you can always do swipe-to-refresh, or tap the navigation icon to scroll to the top of the list.

Download the App

If you want to try out the app without building it, check out the Releases section where you can find the APK and App Bundles for each major release.

Known issues

The video player has known refused-to-fix bugs on Android 14 and 15. You may see the video playback not resizing properly. As a workaround, rotating the device, toggling between full screen or Picture-in-Picture mode can solve the issue.

 

History

This was a code test assignment as a part of the interview process. The task covered common RESTApi, SQLite, RecyclerView, Constraint Layout, MVVM, plus dependency injection and testings.

Although the title carries "Android TV", it has nothing to do with that - this is an Android mobile App as required by the specifications.

The interview process was concluded in October, 2021, but I am still keep on improving the codes for demonstration purpose.

The original XML View version is no longer maintained, you can access to the XML branch here.

Please note that the APIs are supplied by DAZN in 2021 for recruitment purpose. They may not work at any time.

Screenshots

 

 

To-do lists

Planned enhancements are now logged as issues.

High level architecture

  • Kotlin
  • MVVM & clean architecture
  • Jetpack Compose - Single Activity
  • Kotlin Coroutines and Flow
  • Dependency Injection using Dagger Hilt
  • Material 3
  • Dynamic screen layout support using Windows Size Class
  • Jetpack Media 3 video player
  • Gradle Kotlin DSL and Version Catalog
  • Baseline Profile
  • Full unit test and UI (Journey) test suite

Major libraries used

Requirements

  • Android Studio Iguana | 2023.2.1
  • Android device or simulator running Android 9.0+ (API 28)

Building the App

Setting up the keystore

Release builds will be signed if either the keystore file or environment variables are set. Otherwise, the app will be built unsigned.

Local

  • Android Keystore is not being stored in this repository. You need your own Keystore to generate the apk / App Bundle

  • If your project folder is at /app/dazn-code-challenge/, the Keystore file and keystore.properties should be placed at /app/

  • The format of keystore.properties is:

       store=/app/release-key.keystore
       alias=<alias>
       pass=<alias password>
       storePass=<keystore password>
    

CI environment

  • This project has been configured to support automated CI builds.

  • The following environment variables have been set to provide the keystore:

       CI = true
       HOME = <the home directory of the bitrise environment>
       CI_ANDROID_KEYSTORE_PASSWORD = <your keystore password>
       CI_ANDROID_KEYSTORE_ALIAS = <your keystore alias>
       CI_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD = <your keystore private key password>
    

Build and install on the connected device

This app has two build variants: Debug and Release. The most common build commands are:

  • ./gradlew clean installDebug
  • ./gradlew clean instal
  • ./gradlew clean bundleRelease
  • ./gradlew clean assembleRelease

The generated apk(s) will be stored under app/build/outputs/ Debug builds will have an App package name suffix .debug

dazn-code-challenge's People

Contributors

renovate[bot] avatar ryanw-mobile avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar

dazn-code-challenge's Issues

Result use .fold()

Global usage change - use .fold(onSuccess=..., onFailure=...) instead if isSuccess == true

shift initial loading to UIEvent

Right now the composable and viewmodel are firing their own cache fetching and initial reload.

A better way to improve is:

  • Provide a new UIEvent onInitialLoad
  • Let ViewModel to combine within one single loading phase to load the cache then refresh

So that:

  • ViewModel doesn't need init{} for preloading.
  • UI doesn't have to hardcode the refresh() in composable

Surely it needs to check if this can perform the same as having init{}.

provide own API data

Seems the DAZN endpoint is retired. Good time to look for own API for better mock data (and detach DAZN branding)

mapper naming as -> to

Following Kotlin's naming practice, mappers would be the best to use ".toXxx()" as it's effectively an adapter.

Deciding whether to use .toSomething or .asSomething in your Kotlin function naming depends on the nature of the operation your function will perform. Here's a clear guideline on when to use each, based on the behavior of the function:

Use .toSomething

  • Transformation: If your function transforms the data into a completely different format or type, where the output is essentially a new instance created based on the input, use .toSomething. This implies that the operation is typically costly, computationally intensive, or results in a new object that is distinctly different from the source.
  • Example Usage: If you are writing a function that converts a custom data object into a JSON string, this would be a transformation since you are generating a new string representation from the object.
fun CustomData.toJson(): String {
    // Convert the data object to a JSON string
    return jsonParser.serialize(this)
}

Use .asSomething

  • View or Cast: Use .asSomething when the function is casting or changing the perspective on the same data, often without creating a new object. This type of function provides a different way to view or use the same underlying data.
  • Efficiency: These functions are generally more lightweight and efficient because they don't involve creating new objects; they just reinterpret or cast the existing ones.
  • Example Usage: If you are creating a function that allows treating a List as a List, this would be a cast, suitable for asSomething.
fun List<Int>.asNumberList(): List<Number> {
    return this as List<Number>
}

Summary

  • .toSomething: Use when creating a new format or instance from the original data. It indicates a transformation and is suitable for conversions that involve data processing or alteration.
  • .asSomething: Use when providing a different view or cast of the same data without alteration. It indicates a type casting or perspective change, not a data change.
    Following these conventions makes your Kotlin functions more intuitive and aligns with typical expectations from Kotlin developers, enhancing readability and maintainability of your code.

return DTO from data source

Another chat with ChatGPT now suggest data source to return DTO for repository to convert it to domain models.
Small refactoring is needed (affecting unit tests)

Compose rewrite

RW MobiMedia UK Limited now sponsors this repository for KMP rewrite.

It will first migrate to Compose UI, with all the necessary data layer changes - so it will be using SQLDelight and Ktor.

First milestone is an Android App, then iOS support, later it will be Desktop version - given we can find an appropriate video player.

The project namespace and package name, will therefore, change to com.rwmobi.

[chore] Update build script without making keystore mandatory

Requires a quick workaround to make the project buildable (at least for Debug build) when cloned to a fresh environment.

Currently it requires to set up the keystore otherwise it fails the initial Gradle sync.
This has to be workarounded quickly.

Thanks to the feedback again from MSMG.

drawBehind review

There's a new article coming so make sure we checked we applied the same thing

Minor rebranding

To be able to publicise it, it is better to tweak the app logo a little bit.
So later also just in case the API is down, we are good to quickly switch over to another API source for fake schedule and videos.

Media 3 Exoplayer bug fix (Android 14 / API 34)

We should have got most of the PIP implementation right, but we'd better follow the official doc for every minor step to make sure the implementation is right and stick to the recommendations.

On Pixel 7 the player looks to scale the video 1:1 then crop 50% vertically. That's a sign shown when we disabled the control display.

Experimental: Dual pane support

Dependency (#18)

Easy to work around but would be nice to check if any official/better way to support it.

Targets:
Tablet landscape mode: 50% of layout will be blank/video player
Foldable: in expanded mode the upper screen will be the video player.

Not sure about the effort.
Low priority

Complete UI tests

The old fragment-based Espresso tests have all been obsoleted. New tests required.

Media3 Player library memory leak

The library is indeed problematic - as it doesn't clean up properly itself.
Either we write our own Media3 player or trying to see how to fix it

Test coverage improvement

Move the exoplayer enum class to UI model which the test will be counted.
Add back one missing case fot exoplayer view model.

Exoplayer interface and PIP enhancements

Spilt from ticket 44 which we thought it's our fault but it's a bug.

We should have got most of the PIP implementation right, but we'd better follow the official doc for every minor step to make sure the implementation is right and stick to the recommendations.

We can simply some codes knowing the PIP already works.
Also we can implement full-screen mode, and properly handle our custom PIP button animation.

Run ChatGPT over unit test functions naming

Given that we have reached a pretty good coverage already, set up the naming rule on ChatGPT and ask it to run through all the unit tests, then propose standardised test function names.

exoplayer ui minor touchups

The PIP implementation takes time.
We can first port the tiny configuration changes to the video player, so it doesn't show the UI controls when entering the player screen.

Check internet connectivity, and handle network error when it's not available

Right now we just let the API call proceed and populate the error from Retrofit.
It can end up with delays and piling up multiple error messages as it retrys.

There are multiple approaches to tackle this at the UI level or the ViewModel level.

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities

fun isNetworkAvailable(context: Context): Boolean {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val activeNetwork = connectivityManager.activeNetwork ?: return false
    val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
    return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}

OR

class ConnectivityObserver(context: Context) {
    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    val networkAvailable: LiveData<Boolean> = MutableLiveData()

    init {
        val networkCallback = object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                (networkAvailable as MutableLiveData).postValue(true)
            }

            override fun onLost(network: Network) {
                (networkAvailable as MutableLiveData).postValue(false)
            }
        }

        connectivityManager.registerDefaultNetworkCallback(networkCallback)
    }
}

Then in the viewmodel

class MyViewModel(application: Application) : AndroidViewModel(application) {
    private val connectivityObserver = ConnectivityObserver(application)
    val isNetworkAvailable = connectivityObserver.networkAvailable

    fun fetchData() {
        if (isNetworkAvailable.value == true) {
            // Proceed with Retrofit call
        } else {
            // Handle lack of internet connectivity
        }
    }
}

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Ignored or Blocked

These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.

Detected dependencies

github-actions
.github/workflows/dependency-submission.yml
  • actions/checkout v4
  • actions/setup-java v4
  • gradle/actions v3
.github/workflows/main_build.yml
  • timheuer/base64-to-file v1.2
  • actions/checkout v4
  • actions/setup-java v4
  • codecov/codecov-action v4
.github/workflows/renovate_check.yml
  • timheuer/base64-to-file v1.2
  • actions/checkout v4
  • actions/setup-java v4
.github/workflows/tag_create_release.yml
  • timheuer/base64-to-file v1.2
  • actions/checkout v4
  • actions/setup-java v4
  • actions/create-release v1
  • actions/upload-release-asset v1
  • actions/upload-release-asset v1
  • actions/upload-release-asset v1
gradle
gradle.properties
settings.gradle.kts
build.gradle.kts
app/build.gradle.kts
  • composeOptions 1.5.14
baselineprofile/build.gradle.kts
gradle/libs.versions.toml
  • androidx.core:core-ktx 1.13.1
  • androidx.media3:media3-exoplayer 1.3.1
  • androidx.media3:media3-exoplayer-dash 1.3.1
  • androidx.media3:media3-session 1.3.1
  • androidx.media3:media3-test-utils 1.3.1
  • androidx.media3:media3-ui 1.3.1
  • junit:junit 4.13.2
  • androidx.test.espresso:espresso-core 3.5.1
  • androidx.activity:activity-compose 1.9.0
  • androidx.compose:compose-bom 2024.05.00
  • androidx.compose.material3:material3-window-size-class 1.2.1
  • androidx.benchmark:benchmark-macro-junit4 1.2.4
  • androidx.core:core-splashscreen 1.0.1
  • androidx.datastore:datastore-preferences 1.1.1
  • androidx.legacy:legacy-support-v4 1.0.0
  • androidx.lifecycle:lifecycle-extensions 2.2.0
  • androidx.lifecycle:lifecycle-viewmodel-ktx 2.7.0
  • androidx.lifecycle:lifecycle-runtime-ktx 2.7.0
  • androidx.lifecycle:lifecycle-runtime-compose 2.7.0
  • androidx.room:room-compiler 2.6.1
  • androidx.room:room-runtime 2.6.1
  • androidx.room:room-ktx 2.6.1
  • com.squareup.retrofit2:converter-moshi 2.11.0
  • org.jetbrains.kotlinx:kotlinx-coroutines-test 1.8.1
  • com.squareup.okhttp3:logging-interceptor 5.0.0-alpha.14
  • com.squareup.moshi:moshi-adapters 1.15.1
  • com.squareup.moshi:moshi-kotlin 1.15.1
  • com.squareup.moshi:moshi 1.15.1
  • com.squareup.retrofit2:retrofit 2.11.0
  • com.jakewharton.timber:timber 5.0.1
  • io.coil-kt:coil-compose 2.6.0
  • io.coil-kt:coil-gif 2.6.0
  • io.mockk:mockk-android 1.13.11
  • org.jetbrains.kotlinx:kotlinx-coroutines-android 1.8.1
  • io.kotest:kotest-assertions-core 5.9.0
  • org.robolectric:robolectric 4.12.2
  • androidx.test.ext:junit 1.1.5
  • androidx.test:core-ktx 1.5.0
  • androidx.test:rules 1.5.0
  • com.google.dagger:hilt-android 2.51.1
  • com.google.dagger:hilt-compiler 2.51.1
  • com.google.dagger:hilt-android-compiler 2.51.1
  • com.google.dagger:hilt-android-testing 2.51.1
  • androidx.hilt:hilt-navigation-compose 1.2.0
  • com.squareup.leakcanary:leakcanary-android 2.14
  • androidx.compose.material3:material3-adaptive-android 1.0.0-alpha06
  • androidx.test.uiautomator:uiautomator 2.3.0
  • androidx.profileinstaller:profileinstaller 1.3.1
  • com.android.application 8.4.1
  • org.jetbrains.kotlin.android 1.9.24
  • com.google.dagger.hilt.android 2.51.1
  • org.jetbrains.kotlinx.kover 0.7.6
  • org.jlleitschuh.gradle.ktlint 12.1.1
  • com.google.devtools.ksp 1.9.24-1.0.20
  • com.android.test 8.4.1
  • androidx.baselineprofile 1.2.4
gradle-wrapper
gradle/wrapper/gradle-wrapper.properties
  • gradle 8.7

  • Check this box to trigger a request for Renovate to run again on this repository

PIP Playback

Dependency (#18)
Investigate how possible we can provide PIP playback function for Events.

The Media3 library has that but it takes some extra Activity handling to make it work.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.