I started learning Kotlin to develop Android applications and for my first project I have to use MVVM as pattern to separate the presentation layer from the logic. All the articles I've found about MVVM use DataBinding library to bind the data of the models directly to the xml views. I worked a little with DataBinding in Java but I doesn't like it because it is very difficult to find errors when binding is wrong. My questions is if there is another way to use MVVM without DataBinding?
Sure it is. Just abstract example: imagine ViewModel:
class SomeViewModel : ViewModel() {
private lateinit var roadmap: RoleScreenRoadmap
private lateinit var uiScope: CoroutineScope
private val _nameRequired = MutableLiveData<Boolean>()
private val _userName = MutableLiveData<String>()
val nameRequired: LiveData<Boolean>
get() = _nameRequired
val userName: LiveData<String>
get() = _userName
...
}
And now Activity:
class AbstractActivity : AppCompatActivity() {
private lateinit var viewModel: RoleScreenViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_some)
initializeDependencies()
performBindings()
}
private fun initializeDependencies() {
viewModel = ViewModelProviders.of(this).get(AbstractViewModel::class.java)
}
private fun performBindings() {
val lifecycle = ::getLifecycle
viewModel.nameRequired.observe(lifecycle) { nameRequired: Boolean? ->
if (nameRequired!!) {
showNameInputWindow()
}
}
viewModel.userName.observe(lifecycle, ::setTitle)
}
...
}
You can just subscribe during android's lifecycle component initialization and no databinding is ever needed.
Related
I'm doing the same as shown in the documentation here.
I want to Inject the ViewModel into a Composable function (Screen), but I get this error:
Cannot create an instance of class
com.example.blotube.ui.later.LaterViewModel
My ViewModel:
#HiltViewModel
class LaterViewModel #Inject constructor(
private val database: Database
):ViewModel() {
val watchLater=database.videos().getAll()
}
My Composable Function (Screen):
#Composable
fun WatchLater(vm: LaterViewModel = viewModel()){
val videos=vm.watchLater.observeAsState()
val context= LocalContext.current
}
From version androidx.hilt:hilt-navigation-compose:1.0.0-alpha02
you can inject view model into Composable functions by:
hiltViewModel<ViewModelType>()
Example:
#Composable
fun LoginScreen(viewModel: LoginViewModel) {}
LoginScreen(
viewModel = hiltViewModel<LoginViewModel>()
)
Android Developer Documentation compose and hilt
UPDATE:
import androidx.hilt.navigation.compose.hiltViewModel
#Composable
fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel()
){
val videos=vm.watchLater.observeAsState()
val context= LocalContext.current
}
I find the simplest way to do it is inside your composable function.
Add the dependency implementation 'androidx.hilt:hilt-navigation-compose:1.0.0' then;
#Composable
fun Foo(){
val viewModel : Bar = hiltViewModel()
}
then you can use the viewmodel as usual.
You can use ViewModel directly inside Composable function via hiltViewModel()
#Composable
fun WatchLater(vm: LaterViewModel = hiltViewModel()) {
val videos = vm.watchLater.observeAsState()
val context = LocalContext.current
}
Please make sure to add following
Adding androidx.hilt:hilt-navigation-compose dependency to your module level Gradle file. Do check for latest version(tested on 1.0.0-alpha03).
#HiltViewModel to your ViewModel.
#AndroidEntryPoint for the owner using the Composable function.
This appears to be a bug in Jetpack Compose, will probably need to wait for an update on the Jetpack libraries to address it.
As a possible workaround, you could instantiate the viewmodel in your activity and pass it to your composable function
val viewModel: LaterViewModel = viewModel(
"later_viewmodel",
factory = defaultViewModelProviderFactory
)
WatchLater(viewModel)
if you are using the Nav graph component you can also scope your viewmodel to the nav graph using
val viewModel: LaterViewModel = hiltNavGraphViewModel<LaterViewModel>()
WatchLater(viewModel)
base document Inject Hilt In Composable Function
Note: notice to import class
Sample you can used viewModel()
Example:
...
import androidx.lifecycle.viewmodel.compose.viewModel
...
#Composable
fun MyScreen(
viewModel: MyViewModel = viewModel()
) { /* ... */ }
Full Example:
...
import androidx.lifecycle.viewmodel.compose.viewModel
...
#HiltViewModel
class MyViewModel #Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() { /* ... */ }
#Composable
fun MyScreen(
viewModel: MyViewModel = viewModel()
) { /* ... */ }
I'm trying to convert my view models to use the new livedata builder from live data2.0
In all the examples i've seen when you use this new builder pattern they omit how to set parameters take this view model for example, userId is not defined
class UserViewModel : ViewModel() {
private val repository = UserRepository()
val user: LiveData<Response<User>> = liveData {
val data = repository.getUser(userId) // loadUser is a suspend function.
emit(data)
}
}
looks pretty clean on concise, but where do I set the userId its not a function.
before i would expose a function that took a paramater then would update the livedata property.
I was thinking of something like this
class UserViewModel : ViewModel() {
private val repository = UserRepository()
var userId : String? = null
val user: LiveData<Response<User>> = liveData {
val data = repository.getUser(userId) // loadUser is a suspend function.
emit(data)
}
}
and the fragment can set the id, but what if I change the id and want to make another network call?
Hello all can you advice me how to add multiple different adapter in one fragment using dagger ?
#Module
class HomeFragmentModule {
#Provides
internal fun provideHomeInteractor(interactor: HomeInteractor): HomeMVPInteractor = interactor
#Provides
internal fun provideRegisterPresenter(presenter: HomePresenter<HomeView, HomeMVPInteractor>) : HomeMVPPresenter<HomeView, HomeMVPInteractor> = presenter
#Provides
#Named("ppobAdapter")
fun providePpobAdapter(): PpobAdapter = PpobAdapter(ArrayList())
#Provides
#Named("sliderAdapter")
internal fun provideSliderAdapter(): SliderAdapter = SliderAdapter(ArrayList())
} ```
Try this:
HomeFragment : Fragment() {
#Inject
#Named("ppobAdapter")
lateinit var ppobAdapter: PpobAdapter
#Inject
#Name("sliderAdapter")
lateinit var sliderAdapter: SliderAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
DaggerAppComponent
.builder()
.build()
.inject(this)
}
}
With a component that looks like this:
#Component(modules = [HomeFragmentModule::class])
interface AppComponent {
fun inject(fragment: HomeFragment)
}
For a really good intro to dagger I always advise this tutorial series
I think I don't understand well how a Reactive repository and handlers using it work. I have written a special test class only to test the simpliest handler using a repository
#SpringBootTest
class TestRepository() {
#Autowired
lateinit var myRepo: myRepo
#Autowired
lateinit var myHandler: MyHandler
#Test
fun `save with a handler`() {
val myObject = MyObject()
myHandler.save(request).subscribe()
StepVerifier.create (myRepository.count() ) <--this does not work
.expectNext (1L )
.expectComplete().verify()
}
#Test
fun `test only database saving`() {
val object = MyObject()
myRepo.save(myRepo).subscribe()
StepVerifier.create (myRepo.count() ) <-- this works
.expectNext (1L )
.expectComplete().verify()
}
}
my handler and repository are defined in the following way:
#Service
class MyHandler(private val myRepository: MyRepository) {
fun save(object: MyObject): Mono<MyObject> {
return myRepository.save(request)
}
}
#Repository
interface MyRepo : ReactiveMongoRepository<MyObject, String> {
fun save(request: MyObject): Mono<MyObject>
}
I also tried to play with subscribe method but it still does not see the results.
What should I correct?
Use Mono.then function to chain save and count functions and get a resulting Mono:
#Test
fun `save with a handler`() {
val countAfterSave = myHandler.save(MyObject()).then(myRepository.count());
StepVerifier.create(countAfterSave)
.expectNext(1L)
.expectComplete()
.verify()
}
I am moving my project java to kotlin, but got some confusion about KClass and Class
Here is my BaseActivity
abstract class BaseActivity<DB : ViewDataBinding, VM : BaseViewModel> : DaggerAppCompatActivity() {
private lateinit var mCustomDialog: CustomDialog
private lateinit var mViewDataBinding: DB
private lateinit var mViewModel : VM
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set Custom Dialog
mCustomDialog = CustomDialog(this, R.style.LoadingDialogStyle)
// Set ViewModel
mViewModel = ViewModelProviders.of(this, viewModelFactory).get(getViewModelClass().java)
// Set DataBinding
mViewDataBinding = DataBindingUtil.setContentView(this, getLayoutId())
mViewDataBinding.lifecycleOwner = this
mViewDataBinding.setVariable(getBindingVariable(), mViewModel)
mViewDataBinding.executePendingBindings()
// Initialize UI
prepareView(savedInstanceState)
}
#LayoutRes
abstract fun getLayoutId(): Int
protected abstract fun getViewModelClass(): KClass<VM>
abstract fun getBindingVariable(): Int
fun getViewModel(): VM {
return mViewModel
}
fun getViewDataBinding() : DB {
return mViewDataBinding
}
I am using protected abstract fun getViewModelClass(): KClass<VM> function for initializing ViewModel class in the function below
ViewModelProviders.of(this, viewModelFactory).get(getViewModelClass().java)
I use ViewModel in activities by this way
class SplashActivity : BaseActivity<ActivitySplashBinding, SplashViewModel>() {
override fun getViewModelClass(): KClass<SplashViewModel> {
return SplashViewModel::class
}
override fun getLayoutId(): Int {
return R.layout.activity_splash
}
override fun getBindingVariable(): Int {
return BR.vm
}
override fun prepareView(savedInstanceState: Bundle?) {
getViewModel().testLog()
}
}
But when I run the project, I got this error
error: [Dagger/MissingBinding] java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> cannot be provided without an #Provides-annotated method.
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.example.example.MyApp> {
^
java.util.Map<java.lang.Class<? extends androidx.lifecycle.ViewModel>,javax.inject.Provider<androidx.lifecycle.ViewModel>> is injected at
com.example.example.utils.ViewModelFactory(viewModels)
com.example.example.utils.ViewModelFactory is injected at
com.example.example.base.BaseActivity.viewModelFactory
com.example.example.ui.splash.SplashActivity is injected at
dagger.android.AndroidInjector.inject(T) [com.example.example.di.AppComponent ? com.example.example.di.ActivityBindingsModule_SplashActivityInjector$app_debug.SplashActivitySubcomponent]
So I made some research and find out it is about KClass in my ViewModelKey
Here is ViewModelKey
#Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#Retention(AnnotationRetention.RUNTIME)
#MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)
If I do not change my code to Kotlin and use old Java class like this it works properly
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#MapKey
public #interface ViewModelKey {
Class<? extends ViewModel> value();
}
This is my ViewModelFactory class
#Suppress("UNCHECKED_CAST")
class ViewModelFactory #Inject
constructor(private val viewModels: MutableMap<Class<out ViewModel>, #JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val creator = viewModels[modelClass]
?: viewModels.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
?: throw IllegalArgumentException("unknown model class $modelClass")
return try {
creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
My SplashActivityModule
#Module
abstract class SplashActivityModule {
#Binds
#IntoMap
#ViewModelKey(SplashViewModel::class)
internal abstract fun provideSplashViewModel(splashViewModel: SplashViewModel) : ViewModel
}
So how can I use ViewModelKey properly with Kotlin and what is main cause of this error, any help will be appreciated
Your ViewModelKey be like
#MustBeDocumented
#kotlin.annotation.Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
#kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
#MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
As mentioned this question problem is related to Kotlin version. Using higher than 1.3.30 version solves the problem.