Coder Social home page Coder Social logo

androidexample's Introduction

The following is a contrived example application that incorporates Dagger, Kotlin, Coroutines, Flow & Channel, ViewModel, Compose, and Navigation.

It has two primary features:

  1. generate Things by clicking a button
  2. observe Things that are created.

This suggests two use cases, GenerateThingUsecase and ObserveThingsUsecase, that act on a Thing entity.

data class Thing(val id : Int)

interface GenerateThingUsecase {
    suspend fun generateThing()
}

interface ObserveThingsUsecase {
    fun observeThings(): Flow<Thing>
}

We can implement both of these use cases with a single class, ThingsLocalDatasource. In reality there could be one or more remote services responsible for this, and there might be one or more datasources acting as adapters between the remote services and the use cases.

class ThingsLocalDatasource(var currentId : Int, val fakeDelay: Long)
    : ObserveThingsUsecase, GenerateThingUsecase {
    private val thingChannel = Channel<Thing>()

    override suspend fun generateThing() {
        withContext(Dispatchers.IO) { thingChannel.send(Thing(currentId++)) }
    }

    override fun observeThings() : Flow<Thing> {
        return thingChannel.receiveAsFlow().onEach { delay(fakeDelay) }.flowOn(Dispatchers.IO)
    }
}

Next we define an AddThingViewModel to:

  1. mediate ui events (e.g. "Add Thing" button clicked) from a view, and
  2. broadcast data events (new Thing was generated) that a view can respond to.

To accomplish this, our ViewModel constructor requires instances of both use cases (described above).

class AddThingViewModel @Inject constructor(
    private val observeThings : ObserveThingsUsecase,
    private val generateThing : GenerateThingUsecase
) : ViewModel() {
    fun generateThingButtonClicked() { viewModelScope.launch { generateThing.generateThing() } }
    val thingFlow = observeThings.observeThings()
}

The AddThing UI Composable will then:

  1. send button click events to the ViewModel and
  2. set display text when the ViewModel data changes are observed
@Composable
fun AddThing(viewModel: AddThingViewModel) {
    val lastThingText = rememberSaveable { mutableStateOf("") }
    val thingState = viewModel.thingFlow.collectAsState(null)
    thingState.value?.let { lastThingText.value = "$it" }
    AddThing(viewModel::generateThingButtonClicked, lastThingText.value)
}

@Preview
@Composable
fun AddThing(onClick : () -> Unit = {}, text : String = "") {
    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxSize()
    ) {
        Text(text = text, Modifier.padding(8.dp), fontSize = 20.sp)
        Button(onClick = onClick) {
            Text(text = "Add a thing")
        }
    }
}

Now we can use Dagger to generate the graph of dependencies needed to provide the AddThingViewModel. In the Application class we initialize a Dagger AppComponent that binds a @Singleton instance of ThingsLocalDatasource as the implementation for both ObserveThingUsecase and GenerateThingUsecase. Notice both the AppModule and ThingModule are required to fully build the component.
If either one were removed we would get a "missing dependency" compiler error.

class App : Application(), FeatureOneComponent.Provider {
    val component by lazy { DaggerAppComponent.factory().create() }
}

@Module
class AppModule {
    @Provides
    @Named("firstThingId") fun provideThingId() : Int = 1

    @dagger.Component(modules = [AppModule::class, ThingModule::class])
    @Singleton
    interface Component {
        @dagger.Component.Factory
        interface Factory {
            fun create(@BindsInstance @Named("appContext") appContext: Context) : Component
        }

        fun addThingViewModel() : Provider<AddThingViewModel>
    }
}

@Module
class ThingModule {
    @Provides
    @Singleton
    fun ThingsLocalDatasource(@Named("firstThingId") thingId : Int) : ThingsLocalDatasource = ThingsLocalDatasource(thingId, 1000L)

    @Provides
    fun ObserveThingsUsecase(datasource: ThingsLocalDatasource) : ObserveThingsUsecase = datasource

    @Provides
    fun AddThingUsecase(datasource: ThingsLocalDatasource) : GenerateThingUsecase = datasource
}

Code Level Diagram of Injected Object Dependencies for AppComponent

A slanted box indicates a binding instance (@BindsInstance) passed into the component through the factory or builder.

AppComponent

The component above will likely be useful to several views in our application, but for now we will use it only to set up our first view.

class TabFragment1 : Fragment() {
    private val viewModel : AddThingViewModel by viewModelStore {
        requireContext().appComponent().addThingViewModel().get()
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply { setContent { AddThing(viewModel) } }
    }
}

Note: Jetpack ViewModels are lifecycle-aware and thus have special conventions for their initialization. We must use a androidx.lifecycle.ViewModelProvider so that the view model can be paused, reused, and only recreated when required by the lifecycle. The viewModelStore extension method encapsulates the typical approach we want to use for this. We use a Dagger Provider as an object that knows how to create new view model and the ViewModelStoreOwner will decide when to create it.

Code Level Diagram of Injected Object Dependencies for AddThingModule.Component

AddThingComponent

Component Level Diagram of Local Module Dependencies

The components in this diagram are Gradle modules. Green boxes are Android Modules, red boxes are JVM modules, and orange boxes are Kotlin Multiplatform modules. Arrows point to the dependency.

Local Dependency Graph

Component Level Diagram of Remote Module Dependencies

The components in this diagram are Gradle modules. White boxes are local modules, orange boxes are remote modules. Transitive dependencies are excluded. Arrows point to the dependency.

Remote Dependency Graph

Note: The graph generator doesn't currently work with the updated Gradle version that was required to support Compose. So for now, the generated graphs are out of date.

androidexample's People

Contributors

toddway 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.