Skip to content

Commit

Permalink
Scope SavedStateFlowHandle to ViewModel's, create assisted injection …
Browse files Browse the repository at this point in the history
…extension functions, & add CI to run unit tests and build (#11)

* create a savedstateflow-hilt module that automatically scopes SavedStateFlowHandle to the hilt ViewModelScope

* add assisted viewmodel extension functions and update the documentation

* remove app compat since this is unneeded for the hilt module

* add a ci to run units tests and upload results

* refactor samples to have different package names so whole project can build

* update publish version to match the rest of the library
  • Loading branch information
plusmobileapps committed Jan 8, 2022
1 parent b63703b commit b8bd0ae
Show file tree
Hide file tree
Showing 39 changed files with 378 additions and 69 deletions.
7 changes: 7 additions & 0 deletions .github/ci-gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
org.gradle.daemon=false
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx5120m
org.gradle.workers.max=2

kotlin.incremental=false
kotlin.compiler.execution.strategy=in-process
50 changes: 50 additions & 0 deletions .github/workflows/ci-pipeline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Android CI
on:
pull_request:
branches:
- main

jobs:
build-and-run-unit-tests:
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@v2

- name: Copy CI gradle.properties
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties

- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11

- uses: actions/cache@v2
with:
path: |
~/.gradle/caches/modules-*
~/.gradle/caches/jars-*
~/.gradle/caches/build-cache-*
key: gradle-${{ hashFiles('checksum.txt') }}

- name: Build project and run unit tests
run: ./gradlew build testDebug --stacktrace

- name: Publish Unit Test Report
if: always()
uses: mikepenz/action-junit-report@v2
with:
report_paths: '**/build/test-results/testDebugUnitTest/TEST-*.xml'
require_tests: 'true'

- name: Upload build and unit test reports
if: always()
uses: actions/upload-artifact@v2
with:
name: build-and-unit-test-reports
path: |
sample/hilt-di/build/reports
sample/manual-di/build/reports
savedstateflow/build/reports
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:

# Builds the release artifacts of the library
- name: Release build
run: ./gradlew :savedstateflow:assembleRelease :savedstateflow-test:assembleRelease
run: ./gradlew :savedstateflow:assembleRelease :savedstateflow-hilt:assembleRelease :savedstateflow-test:assembleRelease

# Generates other artifacts (javadocJar is optional)
- name: Source jar and dokka
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ buildscript {
appcompat_version = '1.4.0'
core_ktx_version = '1.7.0'
compose_activity_version = '1.4.0'
fragment_ktx_version = '1.4.0'
lifecycle_version = '2.4.0'
livedata_ktx_version = '2.4.0'
viewmodel_ktx_version = '2.4.0'
Expand Down
53 changes: 33 additions & 20 deletions docs/hilt-di.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

[Hilt](https://developer.android.com/training/dependency-injection/hilt-android) is a dependency injection framework built on top of [Dagger](https://dagger.dev/) that helps reduce a lot of boiler plate when injecting dependencies. Hilt can also help reduce the boiler plate when using `SavedStateFlow` by scoping an instance of `SavedStateFlowHandle` to any `ViewModel` that requests it.

## ViewModel
## Hilt ViewModel

A paired down version of the `ViewModel` for this sample is shown below:
The `saved-state-flow-hilt` artifact provides an instance of `SavedStateFlowHandle` to the `ViewModelComponent` out of the box, so it can be declared in any `ViewModel` constructor like so.

```kotlin
@HiltViewModel
Expand Down Expand Up @@ -38,40 +38,53 @@ class MainViewModel @Inject constructor(
}
```

The full version of this `ViewModel` can be found [here](https://github.com/plusmobileapps/SavedStateFlow/blob/main/sample/hilt-di/src/main/java/com/plusmobileapps/savedstateflow/MainViewModel.kt).
The full version of this `ViewModel` can be found [here](https://github.com/plusmobileapps/SavedStateFlow/blob/main/sample/hilt-di/src/main/java/com/plusmobileapps/savedstateflow/MainViewModel.kt).

### Grabbing a Reference to @HiltViewModel

## Scoping SavedStateFlowHandle to ViewModel's
Finally grab a reference to the `ViewModel` using the `by viewmodels { }` delegation function.

Hilt provides the ability of scoping dependencies, so to ensure any `ViewModel` can get a reference to a `SavedStateFlow` the [ViewModel scope](https://dagger.dev/hilt/view-model.html) can be used to scope a `SavedStateFlowHandle` to every `ViewModel`. Considering the following from the documentation:
```kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

> SavedStateHandle is a default binding available to all Hilt View Models. Only dependencies from the ViewModelComponent and its parent components can be provided into the ViewModel
private val viewModel: MainViewModel by viewModels()

The extension function `SavedStateHandle.toSavedStateFlowHandle()` can be used in a module that is installed in the `ViewModelComponent`.
}
```

```kotlin
@InstallIn(ViewModelComponent::class)
@Module
object SavedStateFlowHandleModule {
## Assisted Injection

When using Hilt, it's possible that the `@HiltViewModel` annotation cannot be used when a value needs to be injected into the constructor at runtime. For that, there are a couple of extension methods provided to help inject a `SavedStateFlowHandle` when using Hilt's assisted injection from a `FragmentActivity` or `Fragment`.

@Provides
@ViewModelScoped
fun providesSavedStateFlowHandle(savedStateHandle: SavedStateHandle): SavedStateFlowHandle =
savedStateHandle.toSavedStateFlowHandle()
### Assisted Injected ViewModel

```kotlin
class MyAssistedViewModel @AssistedInject constructor(
@Assisted savedStateFlowHandle: SavedStateFlowHandle,
@Assisted id: String
) : ViewModel() {

@AssistedFactory
interface Factory {
fun create(savedStateFlowHandle: SavedStateFlowHandle, id: String): MyAssistedViewModel
}
}
```

## Grabbing a Reference to ViewModel
### Grab a reference to an Assisted ViewModel

Finally grab a reference to the `ViewModel` using the `by viewmodels { }` delegation function.
Then in a `Fragment` or a `FragmentActivity`, the `by assistedViewModel` method may be used to get a reference to a assisted injected `ViewModel` as this method provides you an instance of a `SavedStateFlowHandle`. There is also a method for fragments to get a `ViewModel` scoped to its `FragmentActivity` if using the `by assistedActivityViewModel {}` method.

```kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

private val viewModel: MainViewModel by viewModels()
class AssistedFragment : Fragment() {
@Inject
lateinit var factory: MyAssistedViewModel.Factory

private val viewModel: MyAssistedViewModel by assistedViewModel { savedStateFlowHandle ->
factory.create(savedStateFlowHandle, arguments?.getString("some-argument-key")!!)
}
}
```

Expand Down
45 changes: 42 additions & 3 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,57 @@
}
```

Then in the `app/build.gradle`, import the dependencies replacing `<version>` with the latest version available. [![Maven Central](https://img.shields.io/maven-central/v/com.plusmobileapps/saved-state-flow?color=blue)](https://search.maven.org/artifact/com.plusmobileapps/saved-state-flow)
For all artifacts listed below, replace `<version>` with the latest version available on Maven Central
[![Maven Central](https://img.shields.io/maven-central/v/com.plusmobileapps/saved-state-flow?color=blue)](https://search.maven.org/artifact/com.plusmobileapps/saved-state-flow) or check out [releases](https://github.com/plusmobileapps/SavedStateFlow/releases) for previous versions. For example:

```
implementation "com.plusmobileapps:saved-state-flow:1.0"
```

## SavedStateFlow

For the basic `SavedStateFlow` API that could be used in any dependency injection framework, please import the following.

=== "Groovy"

``` c
implementation "com.plusmobileapps:saved-state-flow:<version>"
testImplementation "com.plusmobileapps:saved-state-flow-test:<version>"
```

=== "Kotlin"

```kotlin
implementation("com.plusmobileapps:saved-state-flow:<version>")
```

## SavedStateFlow - Hilt

If using [Hilt](https://developer.android.com/training/dependency-injection/hilt-android), this is the only dependency that needs to be imported as it bundles in the `SavedStateFlow` API and allows `SavedStateFlowHandle` to be injected into any `@HiltViewModel`.

=== "Groovy"

``` c
implementation "com.plusmobileapps:saved-state-flow-hilt:<version>"
```

=== "Kotlin"

```kotlin
implementation("com.plusmobileapps:saved-state-flow-hilt:<version>")
```

## Testing

For the `TestSavedStateFlow` artifact, import the following for testing.

=== "Groovy"

``` c
testImplementation "com.plusmobileapps:saved-state-flow-test:<version>"
```

=== "Kotlin"

```kotlin
testImplementation("com.plusmobileapps:saved-state-flow-test:<version>")
```
```
4 changes: 2 additions & 2 deletions sample/hilt-di/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ android {
compileSdk compile_sdk

defaultConfig {
applicationId "com.plusmobileapps.savedstateflow"
applicationId "com.plusmobileapps.savedstateflowhilt"
minSdk min_sdk
targetSdk target_sdk
versionCode 1
Expand Down Expand Up @@ -48,7 +48,7 @@ android {
}

dependencies {
implementation(project(":savedstateflow"))
implementation(project(":savedstateflow-hilt"))
implementation "androidx.core:core-ktx:$core_ktx_version"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
Expand Down
2 changes: 1 addition & 1 deletion sample/hilt-di/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.plusmobileapps.savedstateflow">
package="com.plusmobileapps.savedstateflowhilt">

<application
android:name=".MyApplication"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.plusmobileapps.savedstateflow
package com.plusmobileapps.savedstateflowhilt

import android.os.Bundle
import androidx.activity.ComponentActivity
Expand All @@ -17,7 +17,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.plusmobileapps.savedstateflow.ui.theme.SavedStateFlowTheme
import com.plusmobileapps.savedstateflowhilt.ui.theme.SavedStateFlowTheme
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.plusmobileapps.savedstateflow
package com.plusmobileapps.savedstateflowhilt

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.plusmobileapps.savedstateflow.SavedStateFlow
import com.plusmobileapps.savedstateflow.SavedStateFlowHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.plusmobileapps.savedstateflowhilt

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent

@Module
@InstallIn(SingletonComponent::class)
abstract class NewsModule {

@Binds
abstract fun bindNewsDataSource(
newsRepository: NewsRepository
): NewsDataSource

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.plusmobileapps.savedstateflow
package com.plusmobileapps.savedstateflowhilt

import android.app.Application
import dagger.hilt.android.HiltAndroidApp
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.plusmobileapps.savedstateflow
package com.plusmobileapps.savedstateflowhilt

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.plusmobileapps.savedstateflowhilt.assisted

import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import com.plusmobileapps.savedstateflow_hilt.assistedViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class AssistedFragment : Fragment() {
@Inject
lateinit var factory: MyAssistedViewModel.Factory

private val viewModel: MyAssistedViewModel by assistedViewModel { savedStateFlowHandle ->
factory.create(savedStateFlowHandle, arguments?.getString(ARGUMENTS_KEY)!!)
}

companion object {
const val ARGUMENTS_KEY = "some-argument-key"

fun newInstance(id: String): AssistedFragment = AssistedFragment().apply {
arguments = bundleOf(ARGUMENTS_KEY to id)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.plusmobileapps.savedstateflowhilt.assisted

import androidx.lifecycle.ViewModel
import com.plusmobileapps.savedstateflow.SavedStateFlowHandle
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

class MyAssistedViewModel @AssistedInject constructor(
@Assisted savedStateFlowHandle: SavedStateFlowHandle,
@Assisted id: String
) : ViewModel() {

@AssistedFactory
interface Factory {
fun create(savedStateFlowHandle: SavedStateFlowHandle, id: String): MyAssistedViewModel
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.plusmobileapps.savedstateflow.ui.theme
package com.plusmobileapps.savedstateflowhilt.ui.theme

import androidx.compose.ui.graphics.Color

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.plusmobileapps.savedstateflow.ui.theme
package com.plusmobileapps.savedstateflowhilt.ui.theme

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.plusmobileapps.savedstateflow.ui.theme
package com.plusmobileapps.savedstateflowhilt.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.plusmobileapps.savedstateflow.ui.theme
package com.plusmobileapps.savedstateflowhilt.ui.theme

import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.plusmobileapps.savedstateflow
package com.plusmobileapps.savedstateflowhilt

import app.cash.turbine.test
import com.plusmobileapps.savedstateflow.MainViewModel.Companion.SAVED_STATE_QUERY_KEY
import com.plusmobileapps.savedstateflow.MainViewModel.State
import com.plusmobileapps.savedstateflow.SavedStateFlow
import com.plusmobileapps.savedstateflow.SavedStateFlowHandle
import com.plusmobileapps.savedstateflowhilt.MainViewModel.Companion.SAVED_STATE_QUERY_KEY
import com.plusmobileapps.savedstateflowhilt.MainViewModel.State

import com.plusmobileapps.savedstateflowtest.TestSavedStateFlow
import io.mockk.every
import io.mockk.mockk
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.plusmobileapps.savedstateflow
package com.plusmobileapps.savedstateflowhilt

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.newSingleThreadContext
Expand Down

0 comments on commit b8bd0ae

Please sign in to comment.