Trying to use Dagger with Kotlin on Android. And got the exception:
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property presenter has not been initialized
at com.ad.eartquakekotlin.main.MainFragment.onViewCreated(MainFragment.kt:43)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManager.java:1471)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1784)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManager.java:1852)
at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:802)
at ...
The app is special for testing. I get earthquakes and show them on a device screen.
All I want is
1. Inject the presenter in my Fragment (View)
2. Inject the api in my presenter
There is the structure of my project:
There are two modules and components there, as you can see:
#Module
class ApplicationModule(private val application: Application) {
#Provides
#Singleton
fun provideApplication():Application = application
}
#Module
class MainModule (private val view: MainContract.View) {
#Provides
fun provideView(): MainContract.View {
return view
}
#Provides
fun providePresenter(): MainContract.Presenter {
return MainPresenter(view)
}
}
And components:
#Component(modules = [ApplicationModule::class])
interface ApplicationComponent {
fun inject(application: Application)
fun plus (mainModule: MainModule) : MainComponent
}
and
#Subcomponent(modules = [MainModule::class])
interface MainComponent {
fun inject (view : MainContract.View)
}
There is a contract:
interface MainContract {
interface View {
fun showLoading()
fun hideLoading()
fun showMessage(message: String)
fun showData(data: EarthquakeRootObject)
}
interface Presenter {
fun onDestroy()
fun loadData()
}
}
Application class:
class MainApp: Application() {
companion object {
lateinit var graph: ApplicationComponent
}
override fun onCreate() {
super.onCreate()
buildGraph()
}
private fun buildGraph() {
graph = DaggerApplicationComponent
.builder()
.applicationModule(ApplicationModule(this))
.build()
}
}
Fragment (where I want to use Injection)
class MainFragment : Fragment(), MainContract.View {
private lateinit var earthquakesAdapter: EarthquakeRecyclerViewAdapter
private lateinit var earthquakes: EarthquakeRootObject
#Inject lateinit var presenter: MainContract.Presenter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return container?.inflate(R.layout.fragment_main)
}
override fun onAttach(context: Context?) {
super.onAttach(context)
MainApp.graph.plus(MainModule(this)).inject(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
earthquakesRecyclerView.layoutManager = LinearLayoutManager(context)
earthquakesRecyclerView.setHasFixedSize(true)
presenter.loadData()
}
And my presenter
class MainPresenter (var view: MainContract.View?) : MainContract.Presenter {
private var disposable: Disposable? = null
#Inject lateinit var api : EarthquakeApi
override fun onDestroy() {
disposable?.dispose()
view = null
}
override fun loadData() {
view?.showLoading()
disposable = api.getEarthquakes()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
response ->
view?.showData(response)
view?.hideLoading()
},
{
throwable ->
view?.showMessage(throwable.message ?: "Ошибка")
view?.hideLoading()
}
)
}
What do I do wrong?
You need to replace
fun inject (view : MainContract.View)
with
fun inject(target : MainFragment)
Related
So basically, everything is in the title. I use Dagger2, ViewModelFactory and ViewModelProvider but it always create a new instance of my ViewModel on each screen rotation. I saw that I got the same Factory at each screen rotation but the viewModel got from the ViewModelProvider is always a new one.
I saw this answer but as you can see I'm already using this way to instantiate my VM.
The repo ready to build is available here
ViewModelFactoryModule:
#Module
class ViewModelFactoryModule {
#Singleton
#Provides
fun provideContactListViewModelFactory(repository: Repository) = ContactListViewModel.ContactListViewModelFactory(repository)
}
Instantiation of ViewModel:
class ContactListFragment : BaseFragment() {
private lateinit var adapter: ContactListAdapter
private lateinit var contactDetailDialog:ContactDetailDialog
private lateinit var dataStorage: DataStorage
#Inject
lateinit var viewModelFactory: ContactListViewModel.ContactListViewModelFactory
private lateinit var viewModel: ContactListViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_contact_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, viewModelFactory).get(ContactListViewModel::class.java)
dataStorage = DataStorage(requireContext())
initView()
initListeners()
}
}
ViewModel & Factory:
class ContactListViewModel #Inject constructor(private val repository: Repository) :
ViewModel() {
val contactsLiveData: MutableLiveData<ContactResult> = MutableLiveData()
val loadingState: MutableLiveData<LoadingState> = MutableLiveData()
}
#Singleton
class ContactListViewModelFactory #Inject constructor(private val repository: Repository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ContactListViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return ContactListViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Component class:
#Singleton
#Component(
modules =
[AndroidSupportInjectionModule::class,
NetworkModule::class,
ViewModelFactoryModule::class,
RoomModule::class,
ActivityBuilder::class]
)
interface ContactAppComponent : AndroidInjector<ContactApp> {
fun inject(contactViewHolder: ContactListAdapter.ContactViewHolder)
#Component.Builder
interface Builder {
#BindsInstance
fun application(application: Application): Builder
fun build(): ContactAppComponent
}
}
App Class:
class ContactApp: Application(), HasAndroidInjector {
#Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
contactAppComponent = DaggerContactAppComponent.builder()
.application(this)
.build()
contactAppComponent.inject(this)
}
override fun androidInjector(): AndroidInjector<Any> {
return dispatchingAndroidInjector
}
companion object {
lateinit var contactAppComponent: ContactAppComponent
}
}
BaseFragment:
open class BaseFragment : Fragment() {
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}
}
I am trying to follow MVVM pattern in my Android app but getting error while creating an instance of ViewModel.
Error: Cannot create an instance of class DemoViewModel class.
Here is my code:
DemoFragment.kt:
class DemoFragment : Fragment(R.layout.fragment_demo) {
lateinit var mViewModel: DemoViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewModel=ViewModelProvider(this).get(DemoViewModel::class.java)
mViewModel.getSomeData()
}
}
DemoViewModel.kt:
class DemoViewModel(val demoRepository: DemoRepository) : ViewModel() {
fun getSomeData() {
Log.d("DemoViewModel", "${demoRepository.getData()}")
}
}
DemoRepository.kt:
interface DemoRepository {
fun getData(): Boolean
}
class DemoImpl : DemoRepository {
override fun getData() = false
}
You need to use ViewModelFactory. Because there is "demoRepository" in your primary builder.
class DemoViewModelFactory constructor(private val repository:DemoImpl): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(DemoViewModel::class.java!!)) {
DemoViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}
}
Usage
viewModel = ViewModelProvider(this, DemoViewModelFactory(repositoryObject)).get(DemoViewModel::class.java)
I would encourage you to use "by viewModels()" extension function to create viewModel instance easily. Note that you should add following dependency to use it:
implementation 'androidx.fragment:fragment-ktx:1.2.5'
Sample Fragment Implementation:
class DemoFragment : Fragment() {
// Use the 'by ViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: DemoViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
// Update the UI
})
}
}
Then you can inject an instance of your repository via constructor injection by Dagger or Hilt etc.
I am currently learning MVVM architecture.
I tried to make a BaseActivity class.
My BaseActivity:
abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
AppCompatActivity(),
EventListener {
lateinit var binding: Binding
private var viewModel: ViewModel? = null
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
binding = DataBindingUtil.setContentView(this, layoutid)
this.viewModel = viewModel ?: getViewModel()
binding.setVariable(getBindingVariable(), viewModel)
binding.executePendingBindings()
}
#get: LayoutRes
abstract val layoutid: Int
abstract fun getViewModel(): ViewModel
abstract fun getBindingVariable(): Int
private fun getViewModelClass(): Class<ViewModel> {
val type = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
return type as Class<ViewModel>
}
}
Now I am using this BaseActivity in my SplashActivity:
class SplashActivity : BaseActivity<SplashActivityViewModel, ActivitySplashBinding>() {
private lateinit var viewModel: SplashScreenViewModel
override fun onFailure(message: String) {}
override fun onStarted() {}
override fun onSuccess() {}
override fun getViewModel(): SplashActivityViewModel {
viewModel = ViewModelProvider(this).get(SplashActivityViewModel::class.java)
return viewModel
}
override fun getBindingVariable(): Int {
return BR.splash_viewmodel
}
override val layoutid: Int
get() = R.layout.activity_splash
}
I have used following answer as a reference to implement my BaseActivity.kt: https://stackoverflow.com/questions/55289334/how-to-have-generic-viewmodel-in-baseactivty-class
But I am getting a blank white screen while running the app.
Can someone please tell me what is the problem here or how to make this BaseActivity (without using dependency injection)?
you have overridden the wrong onCreate
override fun onCreate(savedInstanceState: Bundle?) {
I did play around with something like that few years ago, you can find my approach here
I'm using Dagger 2 in clean architecture project, I have 2 fragments. These 2 fragments should be scoped together to share the same instances, but unfortunately, I got empty object in the second fragment.
Application Component
#ApplicationScope
#Component(modules = [ContextModule::class, RetrofitModule::class])
interface ApplicationComponent {
fun exposeRetrofit(): Retrofit
fun exposeContext(): Context
}
Data layer - Repository
class MoviesParsableImpl #Inject constructor(var moviesLocalResult: MoviesLocalResult): MoviesParsable {
private val TAG = javaClass.simpleName
private val fileUtils = FileUtils()
override fun parseMovies() {
Log.d(TAG,"current thread is ".plus(Thread.currentThread().name))
val gson = Gson()
val fileName = "movies.json"
val jsonAsString = MyApplication.appContext.assets.open(fileName).bufferedReader().use{
it.readText()
}
val listType: Type = object : TypeToken<MoviesLocalResult>() {}.type
moviesLocalResult = gson.fromJson(jsonAsString,listType)
Log.d(TAG,"result size ".plus(moviesLocalResult.movies?.size))
}
override fun getParsedMovies(): Results<MoviesLocalResult> {
return Results.Success(moviesLocalResult)
}
}
Repo Module
#Module
interface RepoModule {
#DataComponentScope
#Binds
fun bindsMoviesParsable(moviesParsableImpl: MoviesParsableImpl): MoviesParsable
}
MoviesLocalResultsModule(the result need its instance across different fragments)
#Module
class MoviesLocalResultModule {
#DataComponentScope
#Provides
fun provideMovieLocalResults(): MoviesLocalResult{
return MoviesLocalResult()
}
}
Use case
class AllMoviesUseCase #Inject constructor(private val moviesParsable: MoviesParsable){
fun parseMovies(){
moviesParsable.parseMovies()
}
fun getMovies(): Results<MoviesLocalResult> {
return moviesParsable.getParsedMovies()
}
}
Presentation Component
#PresentationScope
#Component(modules = [ViewModelFactoryModule::class],dependencies = [DataComponent::class])
interface PresentationComponent {
fun exposeViewModel(): ViewModelFactory
}
First ViewModel, where I got the result to be shared with the other fragment when needed
class AllMoviesViewModel #Inject constructor(private val useCase: AllMoviesUseCase):ViewModel() {
private val moviesMutableLiveData = MutableLiveData<Results<MoviesLocalResult>>()
init {
moviesMutableLiveData.postValue(Results.Loading())
}
fun parseJson(){
viewModelScope.launch(Dispatchers.Default){
useCase.parseMovies()
moviesMutableLiveData.postValue(useCase.getMovies())
}
}
fun readMovies(): LiveData<Results<MoviesLocalResult>> {
return moviesMutableLiveData
}
}
Second ViewModel where no need to request data again as it's expected to be scoped
class MovieDetailsViewModel #Inject constructor(private val useCase: AllMoviesUseCase): ViewModel() {
var readMovies = liveData(Dispatchers.IO){
emit(Results.Loading())
val result = useCase.getMovies()
emit(result)
}
}
First Fragment, where data should be requested:
class AllMoviesFragment : Fragment() {
private val TAG = javaClass.simpleName
private lateinit var viewModel: AllMoviesViewModel
private lateinit var adapter: AllMoviesAdapter
private lateinit var layoutManager: LinearLayoutManager
private var ascendingOrder = true
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
DaggerAllMoviesComponent.builder()
.presentationComponent(
DaggerPresentationComponent.builder()
.dataComponent(
DaggerDataComponent.builder()
.applicationComponent(MyApplication.applicationComponent).build()
)
.build()
).build()inject(this)
viewModel = ViewModelProvider(this, viewModelFactory).get(AllMoviesViewModel::class.java)
startMoviesParsing()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_all_movies, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setupRecyclerView()
viewModel.readMovies().observe(viewLifecycleOwner, Observer {
if (it != null) {
when (it) {
is Loading -> {
showResults(false)
}
is Success -> {
showResults(true)
Log.d(TAG, "Data observed ".plus(it.data))
addMoviesList(it.data)
}
is Error -> {
moviesList.snack(getString(R.string.error_fetch_movies))
}
}
}
})
}
Second Fragment, where I expect to get the same instance request in First Fragment as they are scoped.
class MovieDetailsFragment: Fragment() {
val TAG = javaClass.simpleName
#Inject
lateinit var viewModelFactory: ViewModelFactory
lateinit var viewModel: MovieDetailsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val depend = DaggerAllMoviesComponent.builder()
.presentationComponent(
DaggerPresentationComponent.builder()
.dataComponent(
DaggerDataComponent.builder()
.applicationComponent(MyApplication.applicationComponent).build())
.build()
).build()
depend.inject(this)
viewModel = ViewModelProvider(this, viewModelFactory).get(MovieDetailsViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel.readMovies.observe(this, Observer {
if (it!=null){
Log.d(TAG,"Movies returned successfully")
}
})
return super.onCreateView(inflater, container, savedInstanceState)
}
}
Scopes tell a component to cache the results of a binding. It has nothing to do with caching instances of any components. As such, you are always creating a new DataComponent, PresentationComponent, and AllMoviesComponent in your fragments' onCreate methods.
In order to reuse the same AllMoviesComponent instance, you need to store it somewhere. Where you store it can depend on your app architecture, but some options include MyApplication itself, the hosting Activity, or in your navigation graph somehow.
Even after fixing this, you can't guarantee that parseMovies has already been called. The Android system could kill your app at any time, including when MoviesDetailFragment is the current fragment. If that happens and the user navigates back to your app later, any active fragments will be recreated, and you'll still get null.
I have a simple scenario where I do something in fragment and when I receive the LiveData I want to do something in Activity.
ViewModel:
class MyViewModel(application: Application) : AndroidViewModel(application) {
...
fun getUser(id: String): LiveData<User> {
return repository.getUser(id)
}
}
Fragment:
class MyFragment : Fragment() {
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.run {
myViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
} ?: throw Exception("Invalid Activity")
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
button.setOnClickListener {
showProgressBar()
myViewModel.getUser(editText.text.toString()).observe(this, Observer { it ->
//TODO
})
}
}
}
Activity:
class MainActivity : AppCompatActivity() {
private lateinit var myViewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
myViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
//Here I would like to observe the user instance returned from the getUser() method
}
}
So the issue I'm having is that I would like to have an instance of LiveData<User> in MyViewModel so that I could observe it in Activity and Fragment. How can I achieve this?
In Fragemnts (as best practice) Should Use
viewModel.userLiveData.observe(viewLifecycleOwner, Observer {
//your code here
})
In Activity Use
viewModel.userLiveData.observe(this, Observer {
//your code here
})
in class MyViewModel create
val userLiveData =MutableLiveData<User>()
and getter
fun getUserLiveData(id: String): MutableLiveData<User> {
return userLiveData
}
fun getUser(id: String){
val disposableUser = repository.getUser(id)
.subscribe({
userLivedata.postValue(it)
})
}
And in activity or Fragment called
myViewModel.getUserLiveData.observe(this, Observer { it ->
//TODO
})
myViewModel.getUser(...)
And Now in ViewModel you have object User (userLiveData.getValue())
Write method in Activity doSomethingWithUser(user: User)
And in your livedata
myViewModel.getUser(editText.text.toString()).observe(this, Observer { it ->
(requireActivity() as MainActivity).doSomethingWithUser(it)
})