GithubHelp home page GithubHelp logo

emmanueltobi / android-jetpack-playground Goto Github PK

View Code? Open in Web Editor NEW

This project forked from jeppeman/android-jetpack-playground

0.0 1.0 0.0 818 KB

Pet project for cutting edge Android development with Jetpack

License: Apache License 2.0

Kotlin 97.24% Shell 2.76%

android-jetpack-playground's Introduction

CircleCI

Get it on Google Play

android-jetpack-playground

A small video player pet project with the purpose of exploring cutting edge Android development. Some areas of exploration:

  • Dynamic Feature Modules and navigation patterns with them
  • MotionLayout
  • Coroutines
  • Jetpack testing, mainly isolated fragment unit tests that run both on device and the JVM with the same source code

Dynamic Feature Modules and Navigation

In the project I'm discovering navigation patterns when dynamic feature modules are involved, more specifically, top level navigation, cross-feature navigation and app links (deep links). Below is a gif of how the top level navigation turned out:

This is a single activity setup where the top level fragment destinations are located in dynamic feature modules.

This is covered in greater detail in this article.

Dynamic feature module setup

The entry point of each dynamic feature is registered in a common library module as an interface, which looks like this:

// In the common library module
interface Feature<T> {
    fun getMainScreen(): Fragment
    fun getLaunchIntent(context: Context): Intent
    fun inject(dependencies: T)

    data class Info(
            val id: String,
            val name: String,
            @IdRes val actionId: Int
    )
}

getMainScreen() will return the Fragment that is the UI entrypoint for the feature, and inject() will provide the feature with its necessary dependencies. All dynamic feature module definitions must then be an extension of this interface, the video feature in this project is defined as follows in the common library module:

// In the common library module
interface VideoFeature : Feature<VideoFeature.Dependencies> {
    interface Dependencies {
        val okHttpClient: OkHttpClient
        val context: Context
        val handler: Handler
        val backgroundDispatcher: CoroutineDispatcher
    }
}

The implementation of this interface will then reside in the actual dynamic feature module, for the video feature it looks like this:

// In the actual dynamic feature module
@AutoService(VideoFeature::class)
class VideoFeatureImpl : VideoFeature {
    override fun getLaunchIntent(context: Context): Intent {
        return Intent(context, VideoActivity::class.java)
    }

    override fun getMainScreen(): Fragment = createVideoFragment()

    override fun inject(dependencies: VideoFeature.Dependencies) {
        if (::videoComponent.isInitialized) {
            return
        }

        videoComponent = DaggerVideoComponent.factory()
                .create(dependencies, this)
    }
}

The feature instances are resolved at runtime with ServiceLoader, hence the use of @AutoService.

Creating a feature instance

A dynamic feature module can not be declared as a dependency from any other module, therefore VideoFeatureImpl can not be instantiated through normal means from anywhere outsite of the video dynamic feature module. We can either do it through reflection, or using a ServiceLoader. The latter has the nice benefit of removing reflection from the runtime; newer versions of R8 (Google's new code shrinker) will try to make a compiler optimization which replaces calls to ServiceLoader.load(VideoFeature::class.java) with Arrays.asList(new VideoFeatureImpl()), you can find the R8 source code which does this here. Here is what the code to get a feature instance looks like:

inline fun <reified T : Feature<D>, D> FeatureManager.getFeature(
        dependencies: D
): T? {
    return if (isFeatureInstalled<T>()) {
        val serviceIterator = ServiceLoader.load(
                T::class.java,
                T::class.java.classLoader
        ).iterator()

        if (serviceIterator.hasNext()) {
            val feature = serviceIterator.next()
            feature.apply { inject(dependencies) }
        } else {
            null
        }
    } else {
        null
    }
}

Dagger with Dynamic Feature Modules

Commonly with dagger we declare an AppComponent for the application scope, and injection into activities or fragments is done with subcomponents of the AppComponent, this is nice because subcomponents get access to all the dependencies provided by it's parent component. However, we are not able to do this given the fact that the gradle dependency graph has to be inverted (the main app module can not depend on the dynamic feature modules); we must therefore use component dependencies instead. Each feature has a top level component which declares a set of dependencies, the VideoComponent looks like this:

@VideoScope
@Component(
        modules = [
            VideoModule::class,
            VideoApiModule::class,
            VideoRepositoryModule::class
        ],
        dependencies = [VideoFeature.Dependencies::class]
)
interface VideoComponent {
    val videoFragmentComponentFactory: VideoFragmentComponent.Factory

    fun inject(videoFeatureImpl: VideoFeatureImpl)

    @Component.Factory
    interface Factory {
        fun create(
                dependencies: VideoFeature.Dependencies,
                @BindsInstance videoFeatureImpl: VideoFeatureImpl
        ): VideoComponent
    }
}

Recall that VideoFeature.Dependencies was the dependencies that also VideoFeature declared, which resides in a common libary module that the common app module can declare as a dependency; hence we can have our AppComponent provide an object of type VideoFeature.Dependencies, like so:

@Module
object AppModule {

    ...
    
    @Provides
    @JvmStatic
    @Singleton
    fun provideVideoFeatureDependencies(
            context: Context,
            okHttpClient: OkHttpClient,
            handler: Handler,
            backgroundDispatcher: CoroutineDispatcher
    ): VideoFeature.Dependencies =
            object : VideoFeature.Dependencies {
                override val okHttpClient: OkHttpClient = okHttpClient
                override val context: Context = context
                override val handler: Handler = handler
                override val backgroundDispatcher: CoroutineDispatcher = backgroundDispatcher
            }

    ...
    
}

Then we pass this object to the FeatureManager#getFeature method like this, featureManager.getFeature<VideoFeature, VideoFeature.Dependencies>(dependencies).

App Links

App links will unfortunately break if they are declared for an activity in the manifest of a dynamic feature module and the feature is not yet installed; the declaration gets merged into the main manifest but the activity class is not present in the base APK, opening a link pointing to that activity will therefore result in a ClassNotFoundException. To work around this we can have a single entry point from which we launch app links, and from there do the routing to a feature based on the url. In this project I have a class called AppLinkActivity where this is handled. The result is displayed in the gif below:

MotionLayout

This is a really nice tool, complex animations can be created in a fairly simple and declarative way. The editor is also available in the latest canary version of Android Studio, that should help with a lot of the pain points right now, such as the slow workflow when declaring KeyFrames. Not sure if it is worth investing that much time into this tool given that it doesn't seem to be very compatible with Jetpack Compose (which appears to be the future), then again, I'm sure that is something they're aware of at Google and are trying to solve given how much time and resources they've seem to have poured in to this. Below are a few silly animations from the project that showcases MotionLayout.

                                            

Isolated fragment testing for both on-device and JVM with the same source code

After having heard of the write-once-run-everywhere ambitions from the Google IO testing presentations I was very excited. Although Nitrogen is not released yet, I really wanted to take Robolectric 4.0 out for a spin. My ambition was to have fragment unit tests in a shared test folder that would run both instrumented and with Robolectric; since I have some fairly complex UI with animations and orientation changes in the project I thought this would be a tall order, but it was actually achievable in the end with some tinkering. I needed to create a custom shadow for MotionLayout (here) in order to make it work with Robolectric, but apart from that it was mostly smooth sailing. Isolating fragment tests has also been quite messy historically, but the new FragmentScenario simplifies it substantially. Here is an example of a fragment unit test from the project (runs on both JVM and device):

@Test
fun whenPlaying_clickFastForward_shouldDelegateToViewModel() {
    launch {
        whenever(mockPlayingState.initial).thenReturn(true)
        whenever(viewModel.state).thenReturn(mutableLiveDataOf(mockPlayingState))
    }

    onView(withId(R.id.fastForward)).check(matches(isVisibleToUser())).perform(click())

    verify(viewModel).onFastForwardClick()
}

launch is a helper method that calls the new FragmentScenario.launchInContainer() under the hood. The source can be found here and here.

Articles

android-jetpack-playground's People

Contributors

jeppeman avatar

Watchers

 avatar

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.