Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scope SavedStateFlowHandle to ViewModel's, create assisted injection extension functions, & add CI to run unit tests and build #11

Merged
merged 6 commits into from
Jan 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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