I am having a shared View Model for activity and it's fragment.
My view model need's argument to be passed when instantiating from the activity(onCreate only once)
viewModel =ViewModelProviders.of(this,
NoteViewModelFactory(application!!,
uid = intent!!.getStringExtra("uid")!!))
.get(NoteViewModel::class.java)
But from fragment i don't need to pass the argument as i am sure the i have the argument's passed once.
viewModel = ViewModelProviders.of(activity!!).get(NoteViewModel::class.java)
In Koin i tried doing below.
val noteModule = module(override = true) {
viewModel { (id: String) -> NoteViewModel(androidApplication(), id) }
}
in Activity:
private val viewModel: NoteViewModel by viewModel { parametersOf(intent!!.getStringExtra("uid")!!) }
in Fragment:
private val viewModel: NoteViewModel by sharedViewModel()
Application Crashed with below error:
java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.andor.navigate.notepad/com.andor.navigate.notepad.listing.NotesActivity}:
org.koin.core.error.InstanceCreationException: Could not create
instance for
[type:Factory,primary_type:'com.andor.navigate.notepad.core.NoteViewModel']
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2665)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2726)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1477)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
Caused by: org.koin.core.error.InstanceCreationException: Could not create instance for
[type:Factory,primary_type:'com.andor.navigate.notepad.core.NoteViewModel']
at org.koin.core.instance.DefinitionInstance.create(DefinitionInstance.kt:61)
at org.koin.core.instance.FactoryDefinitionInstance.get(FactoryDefinitionInstance.kt:37)
at org.koin.core.definition.BeanDefinition.resolveInstance(BeanDefinition.kt:70)
at org.koin.core.scope.Scope.resolveInstance(Scope.kt:165)
I am not able to understand how to solve this using KOIN.
P.S:i am new to koin DI.
Is there anything wrong when you init koin in application class? I tried your code without any issuses. I'm using koin version 2.0.1
class App : Application() {
override fun onCreate() {
super.onCreate()
val noteModule = module(override = true) {
viewModel { (id: String) -> NoteViewModel(androidApplication(), id) }
}
startKoin {
androidContext(this#App)
modules(
noteModule
)
}
}
}
Activity and fragment:
class MainActivity : AppCompatActivity() {
private val viewModel: NoteViewModel by viewModel { parametersOf(intent!!.getStringExtra("uid")) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d("NoteViewModel", "id: ${viewModel.id}")
supportFragmentManager.beginTransaction().replace(R.id.main_root, Frag()).commit()
}
}
class Frag : Fragment() {
private val viewModel: NoteViewModel by sharedViewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.d("NoteViewModel", "id: ${viewModel.id}")
return inflater.inflate(R.layout.activity_main, container, false)
}
}
View model class:
class NoteViewModel (application: Application, val id: String) : AndroidViewModel(application)
You shouldn't pass these type of arguments in ViewModel constructor. Instead what you can do is on your Activity's onCreate(), you set that passed value to the ViewModel. So when you will access that ViewModel in your fragment, you'll surely have that value already set.
class NoteViewModel (application: Application) : AndroidViewModel(application)
{
var id:String = ""
}
Your koin Module:
val noteModule = module(override = true) {
viewModel { NoteViewModel(androidApplication()) }
}
Activity:
class MainActivity : AppCompatActivity() {
private val viewModel: NoteViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.id = intent?.getStringExtra("uid")?: ""
supportFragmentManager.beginTransaction().replace(R.id.container, MyFrag()).commit()
}
}
Fragment:
class MyFrag : Fragment() {
private val viewModel: NoteViewModel by sharedViewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// your value will be available here.
return inflater.inflate(R.layout.activity_main, container, false)
}
}
Related
I have a parent fragment which fetches a list from API using ViewModel and Retrofit, the ViewModel is injected with Hilt.
After the list gets fetched the parent fragment will pass to its child fragment that is inside of parent fragment.
but the problem is that ViewModel is instantiated one more time in the child fragment.
Parent Fragment
#AndroidEntryPoint
class ParentFragment : Fragment() {
override val mViewModel: URLViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mViewBinding = getViewBinding(inflater, container)
mViewModel.liveData.observe(this, { data ->
{
childFragmentManager.beginTransaction().apply {
replace(
mViewBinding.fragmentContainer.id,
ChildFragment(data)
)
}
commit()
} })
mViewModel.getURL("TEST", "2021-06-18", "2021-07-18", 1 , 0 , -1, false)
return mViewBinding.root
}
}
ChildFragment
#AndroidEntryPoint
class ChildFragment(val data: List<Item>) : Fragment() {
override val mViewModel: URLViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mViewBinding = getViewBinding(inflater, container)
// mViewModel is instantiated again and some all strings properties of it is null.
return mViewBinding.root
}
}
URLViewModel
#HiltViewModel
class URLViewModel #Inject constructor(private val urlApi: URLApi): ViewModel() {
private val _urlLiveData = MutableLiveData<State<Any?>>()
val urlLiveData: LiveData<State<Any?>> = _urlLiveData
var urlName: String? = null
var beginDate: String? = null
var endDate: String? = null
var adultCount = 0
var childrenCount = 0
var airportId = 0
var isRoundTrip = false
init {
Log.e("URLViewModel", "iniialed again" )
}
#ExperimentalStdlibApi
fun getUrl(urlName: String, beginDate: String, endDate: String, adultCount: Int, childCount: Int, airportId: Int, isRoundTrip: Boolean){
Log.e("XXXXXX", "getUrl: called with url of " + urlName )
this.urlName = urlName
this.beginDate = beginDate
this.endDate = endDate
this.adultCount = adultCount
this.childrenCount = childCount
this.airportId = airportId
this.isRoundTrip = isRoundTrip
val mutableLiveData = MutableLiveData<State<Any?>>()
mutableLiveData.value = State.loading()
viewModelScope.launch {
val res = urlApi.getURL(urlName,beginDate,endDate,adultCount,childCount,airportId,isRoundTrip)
Log.e("URLVIewModel", "getUrl: response received" )
_urlLiveData.value = res
}
}
}
when I wanna access some properties like beginDate, they are null, because the ViewModel is instantiated again,
viewModels() delegation create view model against the same instance i.e Fragment's instance in your case. What you need to do is to create a shared View model .
There is helper delegate available for it with ktx libraries.
add the ktx dependency which you already have i guess from here.
implementation "androidx.fragment:fragment-ktx:1.3.4"
And create view model with
private val viewModel by activityViewModels<UrlViewModel>()
You do not have to use activity shared view model. Simply request view model from parent fragment in ChildFragment.
private val viewModel by viewModels<UrlViewModel>(ownerProducer = { requireParentFragment() })
You are trying to use share viewmodel. Try to following code for reference.
#AndroidEntryPoint
class ParentFragment : Fragment() {
private lateinit var viewModel: URLViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(requireActivity()).get(URLViewModel::class.java)
}
}
#AndroidEntryPoint
class ChildFragment : Fragment() {
private lateinit var viewModel: URLViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(requireActivity()).get(URLViewModel::class.java)
}
}
I'm trying to use Dagger hilt in my project. I have an Activity that uses Databinding:
#AndroidEntryPoint
class MainActivity : AppCompatActivity(), SetGreeting {
private lateinit var binding: ActivityMainBinding
#Inject
lateinit var fragmentFactory: FragmentsFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
supportFragmentManager.fragmentFactory = fragmentFactory
...
}
override fun greeting(msg: String) {
binding.greeting.text = msg
}
}
this is how I use greeting interface:
interface SetGreeting {
fun greeting(msg: String)
}
#Module
#InstallIn(ActivityComponent::class)
object SetGreetingModule {
#Provides
fun provideGreeting(): SetGreeting {
return MainActivity()
}
}
which would be used inside of a fragment just like this:
#AndroidEntryPoint
class MainFragment : Fragment() {
private val viewModel: MainViewModel by viewModels()
private lateinit var binding: FragmentMainBinding
#Inject
lateinit var greetings: SetGreeting
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.greeting.observe(viewLifecycleOwner, {
it?.let { msg ->
greetings.greeting(msg)
}
})
}
}
The problem is that when I added Dagger to the project, data binding won't work, and it returns null. So when the override function greeting would be called, I get a null pointer exception.
I think dagger does not call onCreate so
binding = DataBindingUtil.setContentView(this, R.layout.activity_main) is not called then binding will be null.
try to initialize the binding object in init block
init{binding = DataBindingUtil.setContentView(this, R.layout.activity_main) }
i'm making an app and i want to separate my UI logic into multiple UI classes with BaseUi class being lifecycle aware. I'm using Kodein as my DI and i have an issue with fragment.viewLifecycleOwnerLiveData.observe not being called when instance of my ui class is being retrieved by Kodein.
Here is my Fragment class:
class ListFragment : Fragment(), DIAware {
override val di: DI by closestDI()
override val diTrigger: DITrigger = DITrigger()
private var binding: FragmentMoviesBinding? = null
private val fragmentBinding get() = binding
private val kodeinMoviesUi: MoviesUi by instance() //fragment does not observe viewLifecycleOwnerLiveData
private val moviesUi: MoviesUi = MoviesUi(this) //fragment now observe viewLifecycleOwnerLiveData
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMoviesBinding.inflate(inflater, container, false)
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
diTrigger.trigger()
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
}
BaseUi class:
abstract class BaseUi<F : Fragment>(private val fragment: F) : LifecycleObserver {
init {
fragment.viewLifecycleOwnerLiveData.observe(fragment, { subscribeToLifecycle() })
}
private fun subscribeToLifecycle() {
fragment.viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
onViewCreated()
}
})
}
abstract fun onViewCreated()
}
And UiModule:
val uiModule = DI.Module("uiModule") {
bind<ListFragment>() with provider { ListFragment() }
bind<MoviesUi>() with provider { MoviesUi(instance()) }
}
Cross post from https://github.com/Kodein-Framework/Kodein-DI/issues/353
Here is your problem bind<ListFragment>() with provider { ListFragment() }.
You bound the ListFragment with a provider, meaning every time you ask to the container it will create an instance of ListFragment. So, when you inject MoviesUi with private val kodeinMoviesUi: MoviesUi by instance(), it gets another instance of ListFragment.
I suggest that you define the binding for MoviesUi as a factory, waiting to receive a ListFragment instance:
bind<MoviesUi>() with factory {fragment: ListFragment -> MoviesUi(fragment) }
then you can inject it in the ListFragment like:
private val kodeinMoviesUi: MoviesUi by instance(args = this)
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'm dev-ing activity using dagger. In my fragment, I can use this code as below. but when I use this code in activity, I cannot use this code.
private val viewModel by viewModels<NoticeViewModel> { viewModelFactory }
As result I can't initialize viewmodel. how can I initialize activity using dagger?
fragment
class NoticeFragment : DaggerFragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<NoticeViewModel> { viewModelFactory }
private lateinit var viewDataBinding: FragmentNoticeBinding
private var notice = ""
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_notice, container, false)
viewDataBinding = FragmentNoticeBinding.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
return view
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
init()
viewModel.getNotice()
}
private fun init(){
viewModel.notice.observe(this, Observer{
noticeMain.text = it
})
}
}
activity
class ScheduleDialog : DaggerActivity() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<ScheduleDialogViewModel> { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_schedule_dialog)
//viewDataBinding = DataBindingUtil.setContentView( this,R.layout.activity_schedule_dialog)
viewModel.getScheduleById(5)
}
}
MyComponent component = DaggerMyComponent.builder().build();
component.inject(this);
For dependency injection you should have the following code, also in the component code should be
void inject(YourActivity activity);