Branch on viewmodel...

This commit is contained in:
ganfra
2022-12-15 20:16:53 +01:00
parent 8620a86333
commit eba807e37a
15 changed files with 232 additions and 115 deletions

View File

@@ -3,16 +3,6 @@ plugins {
alias(libs.plugins.kapt)
}
/*
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += listOf(
"-opt-in=com.squareup.anvil.annotations.ExperimentalAnvilApi")
}
}
*/
dependencies {
implementation(project(":anvilannotations"))
api(libs.anvil.compiler.api)

View File

@@ -0,0 +1,122 @@
@file:OptIn(ExperimentalAnvilApi::class)
package io.element.android.x.anvilcodegen
import com.google.auto.service.AutoService
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.ExperimentalAnvilApi
import com.squareup.anvil.compiler.api.AnvilCompilationException
import com.squareup.anvil.compiler.api.AnvilContext
import com.squareup.anvil.compiler.api.CodeGenerator
import com.squareup.anvil.compiler.api.GeneratedFile
import com.squareup.anvil.compiler.api.createGeneratedFile
import com.squareup.anvil.compiler.internal.asClassName
import com.squareup.anvil.compiler.internal.buildFile
import com.squareup.anvil.compiler.internal.fqName
import com.squareup.anvil.compiler.internal.reference.ClassReference
import com.squareup.anvil.compiler.internal.reference.asClassName
import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec
import dagger.Binds
import dagger.Module
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.multibindings.IntoMap
import io.element.android.x.anvilannotations.ContributesViewModel
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import java.io.File
/**
* This is an anvil plugin that allows ViewModels to use [ContributesViewModel] alone and let this plugin automatically
* handle the rest of the Dagger wiring required for constructor injection.
*/
@AutoService(CodeGenerator::class)
class ContributesViewModelCodeGenerator : CodeGenerator {
override fun isApplicable(context: AnvilContext): Boolean = true
override fun generateCode(codeGenDir: File, module: ModuleDescriptor, projectFiles: Collection<KtFile>): Collection<GeneratedFile> {
return projectFiles.classAndInnerClassReferences(module)
.filter { it.isAnnotatedWith(ContributesViewModel::class.fqName) }
.flatMap { listOf(generateModule(it, codeGenDir, module), generateAssistedFactory(it, codeGenDir, module)) }
.toList()
}
private fun generateModule(vmClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile {
val generatedPackage = vmClass.packageFqName.toString()
val moduleClassName = "${vmClass.shortName}_Module"
val scope = vmClass.annotations.single { it.fqName == ContributesViewModel::class.fqName }.scope()
val content = FileSpec.buildFile(generatedPackage, moduleClassName) {
addType(
TypeSpec.classBuilder(moduleClassName)
.addModifiers(KModifier.ABSTRACT)
.addAnnotation(Module::class)
.addAnnotation(AnnotationSpec.builder(ContributesTo::class).addMember("%T::class", scope.asClassName()).build())
.addFunction(
FunSpec.builder("bind${vmClass.shortName}Factory")
.addModifiers(KModifier.ABSTRACT)
.addParameter("factory", ClassName(generatedPackage, "${vmClass.shortName}_AssistedFactory"))
.returns(assistedViewModelFactoryFqName.asClassName(module).parameterizedBy(STAR, STAR))
.addAnnotation(Binds::class)
.addAnnotation(IntoMap::class)
.addAnnotation(AnnotationSpec.Companion.builder(viewModelKeyFqName.asClassName(module)).addMember("%T::class", vmClass.asClassName()).build())
.build(),
)
.build(),
)
}
return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content)
}
private fun generateAssistedFactory(vmClass: ClassReference.Psi, codeGenDir: File, module: ModuleDescriptor): GeneratedFile {
val generatedPackage = vmClass.packageFqName.toString()
val assistedFactoryClassName = "${vmClass.shortName}_AssistedFactory"
val constructor = vmClass.constructors.singleOrNull { it.isAnnotatedWith(AssistedInject::class.fqName) }
val assistedParameter = constructor?.parameters?.singleOrNull { it.isAnnotatedWith(Assisted::class.fqName) }
if (constructor == null || assistedParameter == null) {
throw AnvilCompilationException(
"${vmClass.fqName} must have an @AssistedInject constructor with @Assisted initialState: S parameter",
element = vmClass.clazz,
)
}
if (assistedParameter.name != "initialState") {
throw AnvilCompilationException(
"${vmClass.fqName} @Assisted parameter must be named initialState",
element = assistedParameter.parameter,
)
}
val vmClassName = vmClass.asClassName()
val stateClassName = assistedParameter.type().asTypeName()
val content = FileSpec.buildFile(generatedPackage, assistedFactoryClassName) {
addType(
TypeSpec.interfaceBuilder(assistedFactoryClassName)
.addSuperinterface(assistedViewModelFactoryFqName.asClassName(module).parameterizedBy(vmClassName, stateClassName))
.addAnnotation(AssistedFactory::class)
.addFunction(
FunSpec.builder("create")
.addModifiers(KModifier.OVERRIDE, KModifier.ABSTRACT)
.addParameter("initialState", stateClassName)
.returns(vmClassName)
.build(),
)
.build(),
)
}
return createGeneratedFile(codeGenDir, generatedPackage, assistedFactoryClassName, content)
}
companion object {
private val assistedViewModelFactoryFqName = FqName("io.element.android.x.core.di.AssistedViewModelFactory")
private val viewModelKeyFqName = FqName("io.element.android.x.core.di.ViewModelKey")
}
}

View File

@@ -121,6 +121,8 @@ dependencies {
implementation(project(":features:roomlist"))
implementation(project(":features:messages"))
implementation(project(":libraries:di"))
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.0")
implementation(libs.compose.destinations)

View File

@@ -28,6 +28,8 @@ import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import androidx.navigation.NavHostController
import com.airbnb.android.showkase.models.Showkase
import com.airbnb.mvrx.compose.mavericksActivityViewModel
import com.airbnb.mvrx.compose.mavericksViewModel
import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations
@@ -46,14 +48,12 @@ private const val transitionAnimationDuration = 500
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
ElementXTheme {
MainScreen(viewModel = viewModel)
MainScreen(viewModel = mavericksActivityViewModel())
}
}
}

View File

@@ -1,26 +1,41 @@
package io.element.android.x
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.element.android.x.matrix.MatrixInstance
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.core.di.daggerMavericksViewModelFactory
import io.element.android.x.di.AppScope
import io.element.android.x.features.messages.MessagesViewModel
import io.element.android.x.features.messages.model.MessagesViewState
import io.element.android.x.matrix.Matrix
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class MainViewModel : ViewModel() {
private val matrix = MatrixInstance.getInstance()
data class MainState(val fake: Boolean = false) : MavericksState
@ContributesViewModel(AppScope::class)
class MainViewModel @AssistedInject constructor(
private val matrix: Matrix,
@Assisted initialState: MainState
) : MavericksViewModel<MainState>(initialState) {
companion object : MavericksViewModelFactory<MainViewModel, MainState> by daggerMavericksViewModelFactory()
suspend fun isLoggedIn(): Boolean {
return matrix.isLoggedIn().first()
}
fun startSyncIfLogged(){
fun startSyncIfLogged() {
viewModelScope.launch {
if(!isLoggedIn()) return@launch
if (!isLoggedIn()) return@launch
matrix.activeClient().startSync()
}
}
fun stopSyncIfLogged(){
fun stopSyncIfLogged() {
viewModelScope.launch {
if (!isLoggedIn()) return@launch
matrix.activeClient().stopSync()

View File

@@ -4,10 +4,11 @@ import android.content.Context
import com.squareup.anvil.annotations.MergeComponent
import dagger.BindsInstance
import dagger.Component
import io.element.android.x.core.di.DaggerMavericksBindings
@SingleIn(AppScope::class)
@MergeComponent(AppScope::class)
interface AppComponent {
interface AppComponent: DaggerMavericksBindings {
@Component.Factory
interface Factory {

View File

@@ -1,13 +1,22 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.x.features.login"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
implementation(project(":libraries:di"))
implementation(project(":libraries:core"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem"))

View File

@@ -3,16 +3,27 @@ package io.element.android.x.features.login
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Uninitialized
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.core.di.daggerMavericksViewModelFactory
import io.element.android.x.di.AppScope
import io.element.android.x.matrix.Matrix
import io.element.android.x.matrix.MatrixInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class LoginViewModel(initialState: LoginViewState) :
@ContributesViewModel(AppScope::class)
class LoginViewModel @AssistedInject constructor(
private val matrix: Matrix,
@Assisted initialState: LoginViewState) :
MavericksViewModel<LoginViewState>(initialState) {
private val matrix = MatrixInstance.getInstance()
companion object : MavericksViewModelFactory<LoginViewModel, LoginViewState> by daggerMavericksViewModelFactory()
var formState = mutableStateOf(LoginFormState.Default)
private set

View File

@@ -1,13 +1,22 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.x.features.messages"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
implementation(project(":libraries:di"))
implementation(project(":libraries:core"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem"))

View File

@@ -3,19 +3,21 @@ package io.element.android.x.features.messages
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.core.di.daggerMavericksViewModelFactory
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.di.AppScope
import io.element.android.x.features.messages.model.MessagesItemAction
import io.element.android.x.features.messages.model.MessagesItemActionsSheetState
import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.MessagesViewState
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.MatrixInstance
import io.element.android.x.matrix.Matrix
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimeline
import io.element.android.x.matrix.timeline.MatrixTimelineItem
import io.element.android.x.textcomposer.MessageComposerMode
@@ -26,35 +28,20 @@ import kotlinx.coroutines.launch
private const val PAGINATION_COUNT = 50
class MessagesViewModel(
private val client: MatrixClient,
private val room: MatrixRoom,
private val timeline: MatrixTimeline,
private val messageTimelineItemStateFactory: MessageTimelineItemStateFactory,
private val initialState: MessagesViewState
@ContributesViewModel(AppScope::class)
class MessagesViewModel @AssistedInject constructor(
matrix: Matrix,
@Assisted private val initialState: MessagesViewState
) :
MavericksViewModel<MessagesViewState>(initialState) {
companion object : MavericksViewModelFactory<MessagesViewModel, MessagesViewState> {
companion object : MavericksViewModelFactory<MessagesViewModel, MessagesViewState> by daggerMavericksViewModelFactory()
override fun create(
viewModelContext: ViewModelContext,
state: MessagesViewState
): MessagesViewModel? {
val matrix = MatrixInstance.getInstance()
val client = matrix.activeClient()
val room = client.getRoom(state.roomId) ?: return null
val messageTimelineItemStateFactory =
MessageTimelineItemStateFactory(client, room, Dispatchers.Default)
return MessagesViewModel(
client,
room,
room.timeline(),
messageTimelineItemStateFactory,
state
)
}
}
private val client = matrix.activeClient()
private val room = client.getRoom(initialState.roomId)!!
private val messageTimelineItemStateFactory =
MessageTimelineItemStateFactory(client, room, Dispatchers.Default)
private val timeline = room.timeline()
private val timelineCallback = object : MatrixTimeline.Callback {
override fun onPushedTimelineItem(timelineItem: MatrixTimelineItem) {

View File

@@ -2,32 +2,24 @@ package io.element.android.x.features.messages.textcomposer
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.core.data.StableCharSequence
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.MatrixInstance
import io.element.android.x.core.di.daggerMavericksViewModelFactory
import io.element.android.x.di.AppScope
import io.element.android.x.matrix.Matrix
class MessageComposerViewModel(
private val client: MatrixClient,
private val initialState: MessageComposerViewState
@ContributesViewModel(AppScope::class)
class MessageComposerViewModel @AssistedInject constructor(
private val matrix: Matrix,
@Assisted private val initialState: MessageComposerViewState
) : MavericksViewModel<MessageComposerViewState>(initialState) {
companion object :
MavericksViewModelFactory<MessageComposerViewModel, MessageComposerViewState> {
MavericksViewModelFactory<MessageComposerViewModel, MessageComposerViewState> by daggerMavericksViewModelFactory()
override fun create(
viewModelContext: ViewModelContext,
state: MessageComposerViewState
): MessageComposerViewModel? {
val matrix = MatrixInstance.getInstance()
val client = matrix.activeClient()
return MessageComposerViewModel(
client,
state
)
}
}
private val client = matrix.activeClient()
fun onComposerFullScreenChange() {
setState {

View File

@@ -1,13 +1,21 @@
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.ksp)
alias(libs.plugins.anvil)
}
android {
namespace = "io.element.android.x.features.roomlist"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
implementation(project(":anvilannotations"))
anvil(project(":anvilcodegen"))
implementation(project(":libraries:di"))
implementation(project(":libraries:core"))
implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem"))

View File

@@ -1,13 +1,19 @@
package io.element.android.x.features.roomlist
import com.airbnb.mvrx.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.core.data.parallelMap
import io.element.android.x.core.di.daggerMavericksViewModelFactory
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.di.AppScope
import io.element.android.x.features.roomlist.model.MatrixUser
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListRoomSummaryPlaceholders
import io.element.android.x.features.roomlist.model.RoomListViewState
import io.element.android.x.matrix.Matrix
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.MatrixInstance
import io.element.android.x.matrix.media.MediaResolver
@@ -22,28 +28,17 @@ import kotlinx.coroutines.launch
private const val extendedRangeSize = 40
class RoomListViewModel(
private val client: MatrixClient,
initialState: RoomListViewState
@ContributesViewModel(AppScope::class)
class RoomListViewModel @AssistedInject constructor(
matrix: Matrix,
@Assisted initialState: RoomListViewState
) :
MavericksViewModel<RoomListViewState>(initialState) {
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> {
override fun create(
viewModelContext: ViewModelContext,
state: RoomListViewState
): RoomListViewModel {
val matrix = MatrixInstance.getInstance()
val client = matrix.activeClient()
return RoomListViewModel(
client,
state
)
}
}
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by daggerMavericksViewModelFactory()
private val lastMessageFormatter = LastMessageFormatter()
private val client = matrix.activeClient()
init {
handleInit()

View File

@@ -1,19 +1,3 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.x.core.di
import com.airbnb.mvrx.MavericksState

View File

@@ -22,7 +22,7 @@ import com.airbnb.mvrx.ViewModelContext
* }
*/
inline fun <reified VM : MavericksViewModel<S>, S : MavericksState> daggerMavericksViewModelFactory() = DaggerMavericksViewModelFactory<VM, S>(VM::class.java)
inline fun <reified VM : MavericksViewModel<S>, S : MavericksState> daggerMavericksViewModelFactory() = DaggerMavericksViewModelFactory(VM::class.java)
/**
* A [MavericksViewModelFactory] makes it easy to create instances of a ViewModel
@@ -55,14 +55,6 @@ class DaggerMavericksViewModelFactory<VM : MavericksViewModel<S>, S : MavericksS
}
}
/**
* These Anvil/Dagger bindings are used by [DaggerMavericksViewModelFactory]. The factory will find the nearest [DaggerComponentOwner]
* that implements these bindings. It will then attempt to retrieve the [AssistedViewModelFactory] for the given ViewModel class.
*
* In this example, this bindings class is implemented by [com.airbnb.mvrx.sample.anvil.feature.ExampleFeatureComponent] because
* it provides the [com.airbnb.mvrx.sample.anvil.feature.ExampleFeatureViewModel]. Any component that will generate ViewModels should
* either implement this directly or have this added via `@ContributesTo(YourScope::class)`.
*/
interface DaggerMavericksBindings {
fun viewModelFactories(): Map<Class<out MavericksViewModel<*>>, AssistedViewModelFactory<*, *>>
}