Skip to content
This repository has been archived by the owner on Jan 13, 2022. It is now read-only.

Commit

Permalink
Add support for androidx.fragment.app.FragmentFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
svenjacobs committed Jan 23, 2020
1 parent 9445425 commit b1b6f82
Show file tree
Hide file tree
Showing 22 changed files with 249 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ cache:

deploy:
provider: script
script: ./gradlew :core:bintrayUpload :android:bintrayUpload :androidx-viewmodel:bintrayUpload :androidx-viewmodel-savedstate:bintrayUpload
script: ./gradlew :core:bintrayUpload :android:bintrayUpload :androidx-fragment:bintrayUpload :androidx-viewmodel:bintrayUpload :androidx-viewmodel-savedstate:bintrayUpload
skip_cleanup: true
on:
branch: master
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## Version 1.11.0

_2020-01-23_

* Add support for Fragment instantiation through Katana with new artifact `katana-androidx-fragment` and
[KatanaFragmentFactory](./androidx-fragment/src/main/kotlin/org/rewedigital/katana/androidx/fragment/KatanaFragmentFactory.kt)
(also see [FragmentFactory](https://developer.android.com/reference/androidx/fragment/app/FragmentFactory) of `androidx.fragment:fragment:1.2.0`)

## Version 1.10.0

_2020-01-03_
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ Katana consists of a core library and several additional libraries that extend
Katana's functionality. All artifact are published in group
`org.rewedigital.katana`. Here's a quick overview of all available artifacts:

| Artifact | Description |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| katana-core | Provides core functionality. Suitable for plain Kotlin (server side) and Android. |
| [katana-android](./android) | Android-specific extensions like modules for Activity and Fragment, KatanaFragment etc. |
| [katana-androidx-viewmodel](./androidx-viewmodel) | Enables dependency injection for AndroidX ViewModel. |
| [katana-androidx-viewmodel-savedstate](./androidx-viewmodel-savedstate) | Support for yet experimental AndroidX ViewModel with SavedState. |
| Artifact | Description |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| katana-core | Provides core functionality. Suitable for plain Kotlin (server side) and Android. |
| [katana-android](./android) | Android-specific extensions like modules for `Activity` and `Fragment`, `KatanaFragment` etc. |
| [katana-androidx-fragment](./androidx-fragment) | Additional support for `androidx.fragment` providing a Katana-based `FragmentFactory`. |
| [katana-androidx-viewmodel](./androidx-viewmodel) | Enables dependency injection for AndroidX ViewModel. |
| [katana-androidx-viewmodel-savedstate](./androidx-viewmodel-savedstate) | Support for AndroidX ViewModel with SavedState. |

## Help & Contribution

Expand Down
11 changes: 6 additions & 5 deletions android-example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,21 @@ android {

applicationId = "org.rewedigital.katana.android.example"
versionCode = 1
versionName = "1.10.0"
versionName = "1.11.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}

dependencies {
implementation("org.rewedigital.katana:katana-android:1.10.0")
implementation("org.rewedigital.katana:katana-androidx-viewmodel-savedstate:1.10.0-rc04")
implementation("org.rewedigital.katana:katana-android:1.11.0")
implementation("org.rewedigital.katana:katana-androidx-fragment:1.11.0")
implementation("org.rewedigital.katana:katana-androidx-viewmodel-savedstate:1.11.0-rc03")
implementation("androidx.appcompat:appcompat:1.1.0")
implementation("androidx.constraintlayout:constraintlayout:1.1.3")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.3.61")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3")
implementation("org.jetbrains.anko:anko-coroutines:0.10.8")
implementation("com.squareup.retrofit2:retrofit:2.6.2")
implementation("com.squareup.retrofit2:converter-moshi:2.6.2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.rewedigital.katana.dsl.factory

const val FRAGMENT_DEPENDENCY1 = "FRAGMENT_DEPENDENCY1"

val firstFragmentModule = Module {
val FirstFragmentModule = Module {

factory(name = FRAGMENT_DEPENDENCY1) { "FRAGMENT_DEPENDENCY1" }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.rewedigital.katana.dsl.factory

const val SOME_DEPENDENCY = "SOME_DEPENDENCY"

val fragmentActivityModule = Module {
val FragmentActivityModule = Module {

factory(name = SOME_DEPENDENCY) { "SOME_DEPENDENCY" }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.rewedigital.katana.android.example.fragment

import org.rewedigital.katana.Module
import org.rewedigital.katana.android.example.fragment.view.FirstFragment
import org.rewedigital.katana.android.example.fragment.view.SecondFragment
import org.rewedigital.katana.androidx.fragment.KatanaFragmentFactory
import org.rewedigital.katana.dsl.component
import org.rewedigital.katana.dsl.factory
import org.rewedigital.katana.dsl.singleton

val FragmentFactoryModule = Module(
name = "FragmentFactoryModule"
) {

singleton {
KatanaFragmentFactory(component)
.handlesFragment<FirstFragment>()
.handlesFragment<SecondFragment>()
}

factory { FirstFragment(component) }

factory { SecondFragment(component) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.rewedigital.katana.dsl.get

const val FRAGMENT_DEPENDENCY2 = "FRAGMENT_DEPENDENCY2"

val secondFragmentModule = Module {
val SecondFragmentModule = Module {

factory(name = FRAGMENT_DEPENDENCY2) { "FRAGMENT_DEPENDENCY2" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import org.rewedigital.katana.Component
import org.rewedigital.katana.KatanaTrait
import org.rewedigital.katana.android.example.R
import org.rewedigital.katana.android.example.fragment.FRAGMENT_DEPENDENCY1
import org.rewedigital.katana.android.example.fragment.FirstFragmentModule
import org.rewedigital.katana.android.example.fragment.SOME_DEPENDENCY
import org.rewedigital.katana.android.example.fragment.firstFragmentModule
import org.rewedigital.katana.android.fragment.KatanaFragmentDelegate
import org.rewedigital.katana.android.fragment.fragmentDelegate
import org.rewedigital.katana.injectNow
Expand All @@ -24,7 +24,9 @@ import org.rewedigital.katana.injectNow
* @see KatanaFragmentDelegate
* @see SecondFragment
*/
class FirstFragment : Fragment(), KatanaTrait {
class FirstFragment(
superComponent: Component
) : Fragment(), KatanaTrait {

private val fragmentDelegate: KatanaFragmentDelegate<FirstFragment>

Expand All @@ -33,8 +35,8 @@ class FirstFragment : Fragment(), KatanaTrait {
private lateinit var fragmentDependency: String

init {
fragmentDelegate = fragmentDelegate { activity, _ ->
component = (activity as KatanaTrait).component + firstFragmentModule
fragmentDelegate = fragmentDelegate { _, _ ->
component = superComponent + FirstFragmentModule

activityDependency = injectNow(SOME_DEPENDENCY)
fragmentDependency = injectNow(FRAGMENT_DEPENDENCY1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import androidx.appcompat.app.AppCompatActivity
import org.rewedigital.katana.Component
import org.rewedigital.katana.KatanaTrait
import org.rewedigital.katana.android.example.R
import org.rewedigital.katana.android.example.fragment.fragmentActivityModule
import org.rewedigital.katana.android.example.fragment.FragmentActivityModule
import org.rewedigital.katana.android.example.fragment.FragmentFactoryModule
import org.rewedigital.katana.android.modules.ActivityModule
import org.rewedigital.katana.androidx.fragment.KatanaFragmentFactory
import org.rewedigital.katana.inject

/**
* @see FirstFragment
Expand All @@ -17,11 +20,17 @@ class FragmentActivity : AppCompatActivity(), KatanaTrait {
override val component: Component = Component(
modules = listOf(
ActivityModule(this),
fragmentActivityModule
FragmentFactoryModule,
FragmentActivityModule
)
)

private val fragmentFactory by inject<KatanaFragmentFactory>()

override fun onCreate(savedInstanceState: Bundle?) {
// Must be set **before** super call for Fragment instantiation after orientation change
supportFragmentManager.fragmentFactory = fragmentFactory

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_fragment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import kotlinx.android.synthetic.main.fragment_second.view.*
import org.rewedigital.katana.Component
import org.rewedigital.katana.KatanaTrait
import org.rewedigital.katana.android.example.R
import org.rewedigital.katana.android.example.fragment.SecondFragmentModule
import org.rewedigital.katana.android.example.fragment.inject.Container
import org.rewedigital.katana.android.example.fragment.model.SecondFragmentViewModel
import org.rewedigital.katana.android.example.fragment.secondFragmentModule
import org.rewedigital.katana.android.fragment.KatanaFragment
import org.rewedigital.katana.androidx.viewmodel.savedstate.viewModelSavedStateNow
import org.rewedigital.katana.injectNow
Expand All @@ -23,10 +23,12 @@ import org.rewedigital.katana.injectNow
* Also this fragment showcases Katana's support for [androidx.lifecycle.ViewModel].
*
* @see FirstFragment
* @see secondFragmentModule
* @see SecondFragmentModule
* @see KatanaFragment
*/
class SecondFragment : KatanaFragment(), KatanaTrait {
class SecondFragment(
private val superComponent: Component
) : KatanaFragment(), KatanaTrait {

override lateinit var component: Component
private lateinit var container: Container
Expand All @@ -45,7 +47,7 @@ class SecondFragment : KatanaFragment(), KatanaTrait {
}

override fun onInject(activity: Activity, savedInstanceState: Bundle?) {
component = (activity as KatanaTrait).component + secondFragmentModule
component = superComponent + SecondFragmentModule

container = injectNow()
viewModel = viewModelSavedStateNow()
Expand Down
37 changes: 37 additions & 0 deletions androidx-fragment/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Fragment constructor injection with FragmentFactory

This artifact provides additional support for `androidx.fragment` enabling constructor injection in Fragments with
`KatanaFragmentFactory`.

```kotlin
// In a module:

val module = Module {

singleton {
KatanaFragmentFactory(component)
.handlesFragment<FirstFragment>()
.handlesFragment<SecondFragment>(name = "SecondFragment")
}

factory { FirstFragment(get()) }

factory(name = "SecondFragment") { SecondFragment(get(), get()) }
}

// ... then in an Activity:

class MyActivity : AppCompatActivity() {

private val fragmentFactory by applicationComponent.inject<KatanaFragmentFactory>()

override fun onCreate(savedInstanceState: Bundle?) {
// Must be set **before** super call for Fragment instantiation after orientation change
supportFragmentManager.fragmentFactory = fragmentFactory

super.onCreate(savedInstanceState)

// ...
}
}
```
15 changes: 15 additions & 0 deletions androidx-fragment/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
plugins {
`base-android-plugin`
}

configureBase(
artifactName = "katana-androidx-fragment",
sourcePath = android.sourceSets["main"].java.srcDirs,
publicationComponent = components["android"]
)

dependencies {
api(project(":core"))
api(Dependencies.androidXCollection)
api(Dependencies.androidXFragment)
}
2 changes: 2 additions & 0 deletions androidx-fragment/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="org.rewedigital.katana.androidx.fragment"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package org.rewedigital.katana.androidx.fragment

import androidx.collection.ArrayMap
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory
import org.rewedigital.katana.Component
import org.rewedigital.katana.KatanaException

/**
* A [FragmentFactory] that utilizes Katana for [Fragment] instance resolution.
*
* The factory can only provide instances of Fragments that have been registered via [handlesFragment] or
* [handlesFragmentVia]. The referred component(s) must provide bindings for these Fragments.
*
* ```
* val module = Module {
*
* singleton {
* KatanaFragmentFactory(component)
* .handlesFragment<FirstFragment>()
* .handlesFragment<SecondFragment>(name = "SecondFragment")
* }
*
* factory { FirstFragment(get()) }
*
* factory(name = "SecondFragment") { SecondFragment(get(), get()) }
* }
* ```
*
* @param defaultComponent Optional default [Component] required for [handlesFragment]
* @param delegateToSuper If `true`, will delegate requests to super FragmentFactory if this factory cannot resolve Fragment. Might be useful for third-party Fragments like `NavHostFragment` for instance.
*
* @see handlesFragment
* @see handlesFragmentVia
*/
@Suppress("unused", "MemberVisibilityCanBePrivate")
class KatanaFragmentFactory(
defaultComponent: Component? = null,
private val delegateToSuper: Boolean = true
) : FragmentFactory() {

@PublishedApi
internal val defaultComponentProvider =
defaultComponent?.let { { it } }

@PublishedApi
internal val providers = ArrayMap<String, () -> Fragment>()

/**
* Declare that this factory handles Fragment instantiations of given type.
*
* A default [Component] **must** be passed to constructor of factory in order for this to work.
* The default component must provide a binding for the requested Fragment which is bound to the class or
* optional name. For instance a module might declare:
*
* ```
* factory { MyFragment(get(), get()) }
* ```
*
* Use [handlesFragmentVia] for adding a Fragment provider associated with a different component.
*
* @see handlesFragmentVia
*/
inline fun <reified T : Fragment> handlesFragment(
name: String? = null
): KatanaFragmentFactory {
defaultComponentProvider?.let { component ->
val fragmentClassName = T::class.java.name
providers[fragmentClassName] = { component().injectNow<T>(name = name) }
} ?: throw KatanaException("No default component passed to constructor of KatanaFragmentFactory")
return this
}

/**
* Declare that this factory handles Fragment instantiations via provided [Component].
*
* The supplied [Component] must provide a binding for the requested Fragment which is bound to the class or
* optional name. For instance a module might declare:
*
* ```
* factory { MyFragment(get(), get()) }
* ```
*
* @see handlesFragment
*/
inline fun <reified T : Fragment> handlesFragmentVia(
name: String? = null,
noinline component: () -> Component
): KatanaFragmentFactory {
val fragmentClassName = T::class.java.name
providers[fragmentClassName] = { component().injectNow<T>(name = name) }
return this
}

/**
* Create a new instance of a Fragment with the given class name.
*
* If this Fragment has not been registered via [handlesFragment] or [handlesFragmentVia] and [delegateToSuper] is `true`,
* will call super [FragmentFactory] for resolution. Else throws [KatanaException].
*
* @throws KatanaException
*/
override fun instantiate(classLoader: ClassLoader, className: String) =
providers[className]?.let { provider ->
provider()
} ?: if (delegateToSuper)
super.instantiate(
classLoader,
className
) else throw KatanaException("No Fragment provider found for class $className (delegateToSuper == false)")
}
Empty file.
Loading

0 comments on commit b1b6f82

Please sign in to comment.