I have an app with a single activity but with many Fragments. I am using ViewModel for my Activity-Fragment communication. Lately, I am using Hilt, and I am having a problem now communicating between my activity and fragments.
My Viewmodel
#HiltViewModel
class AppViewModel #Inject internal constructor(
): ViewModel() {
private var _data = MutableLiveData<String>()
val data: LiveData<String>
get() = _data
fun insertData(dataStr: String) {
_data.value = dataStr
}
}
My MainActivity
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mViewModel: AppViewModel by viewModels()
private var dataString: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mViewModel.data.observe(this, {
dataString = it
})
}
}
One of my Fragments
#AndroidEntryPoint
class ReportFragment : Fragment() {
private val reportViewModel: ReportViewModel by viewModels()
private val appViewModel: AppViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
...
appViewModel.insertData("Hello")
...
}
}
When I run the app, I am getting null as a result of data. Any solution to solve this?
Not sure if this is the exact issue, but you get the ViewModel inside your fragment using by activityViewModels<AppViewModel> and not by viewModels
EDIT:
Also, I just noticed you are using an internal constructor. Try using only inject constructor once and let me know if it fixed it for you :)
Related
I am currently reading data from a Bluetooth Sensor, hence the data changes in real-time and continuously changes. I have stored the data in a variable: liveData:ByteArray
Now I am trying to send liveData from MainActivity to Sensordisplayfragment.
UPDATE
Based on #CTD's comment, this is what I have tried, unfortunately I do not have much knowledge on viewModel, and online research is just confusing as there seems to be many methods to implement a viewModel.
In my MainActivity class where variable liveData is stored:
val model:MyViewModel by viewModels()
private fun processLiveData(liveData : ByteArray){
livedata = liveData
model.uploadData(livedata)
}
In MyViewModel.class where the viewModel is at:
class MyViewModel: ViewModel() {
private val realtimedata = MutableLiveData<ByteArray>()
fun uploadData(data:ByteArray){
realtimedata.value = data
}
fun loadData():LiveData<ByteArray>{
return realtimedata
}
}
Finally, in my Sensordisplay fragment where I am fetching the data:
val model:MyViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
model.loadData().observe(viewLifecycleOwner,Observer<ByteArray>{
passandprocessLiveData(it)
})
return inflater.inflate(R.layout.sensordisplay, container, false)
}
override fun onResume(){
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
model.loadData().observe(viewLifecycleOwner,Observer<ByteArray>{
passandprocessLiveData(it)
})
super.onResume()
}
fun passandprocessLiveData(data:Bytearray){
//extract information from data and make
//cardviews move in realtime according to the extracted data
}
Unfortunately,nothing is getting transferred and my cardviews are not moving. I can guarantee there is no error in the moving of the cardview codes. Anyone able to advice on what I can add? Apparently there is an init() function that I need to use.
class MyViewModel : ViewModel() {
private val realtimedata = MutableLiveData<ByteArray>()
val sensorData: LiveData<ByteArray> = realtimedata
fun update(data: ByteArray){
realtimedata.value = data
}
}
class MainActivity: Activity() {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
bluetoothSensorCallBack { data ->
// Update the realtimedata
viewModel.update(data)
}
}
}
class SensordisplayFragment : Fragment() {
// Use the 'by activityViewModels()' Kotlin property delegate
// from the fragment-ktx artifact
private val model: MyViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.sensorData.observe(viewLifecycleOwner, Observer<ByteArray> { data ->
// Update the UI
})
}
}
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 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 am trying out flows and trying to see how they can be converted to mvvm with android view models. Here is what I tried first to test it out :
class HomeViewModel : ViewModel() {
private lateinit var glucoseFlow: LiveData<Int>
var _glucoseFlow = MutableLiveData<Int>()
fun getGlucoseFlow() {
glucoseFlow = flowOf(1,2).asLiveData()
_glucoseFlow.value = glucoseFlow.value
}
}
class HomeFragment : Fragment() {
private lateinit var viewModel: HomeViewModel
override fun onCreateView (
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.home_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(HomeViewModel::class.java)
viewModel._glucoseFlow.observe(this, Observer {
handleUpdate(it)
})
viewModel.getGlucoseFlow()
}
private fun handleUpdate(reading : Int) {
glucose_reading.text = reading.toString()
}
}
I get a null for the reading number however any ideas ?
This happens because you are trying to assign glucoseFlow.value to _glucoseFlow.value directly, I guess you should use a MediatorLiveData<Int>, however this is not my final suggestion.
You can solve it if you collect flow items and then assign them to your private variable.
// For private variables, prefer use underscore prefix, as well MutableLiveData for assignable values.
private val _glucoseFlow = MutableLiveData<Int>()
// For public variables, prefer use LiveData just to read values.
val glucoseFlow: LiveData<Int> get() = _glucoseFlow
fun getGlucoseFlow() {
viewModelScope.launch {
flowOf(1, 2)
.collect {
_glucoseFlow.value = it
}
}
}
Having the before implementation over the HomeViewModel, start to observe your public glucoseFlow from HomeFragment and you will be able to receive non-null sequence values (1 and then 2).
If you are using databinding, do not forget specify the fragment view as the lifecycle owner of the binding so that the binding can observe LiveData updates.
class HomeFragment : Fragment() {
...
binding.lifecycleOwner = viewLifecycleOwner
}
I have a listview in my fragment UI that its elements set depend on status of a value that come from a viewmodel LiveData attribute.
I want to create instrumental test for the fragment which englobes 3 scenarios test case related to the value set of that attribute and I don't where to start.
My code should kind look like below :
class MyViewModel : ViewModel() {
var status = MutableLiveData("")
}
class MyFragment : Fragment() {
private lateinit var myViewModel: MyViewModel
private lateinit var myListView: ListView
override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
myViewModel =
ViewModelProviders.of(this, ViewModelProvider.Factory).get(MyViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
when (myViewModel?.status) {
"status1":
setListContent(items1)
"status2":
setListContent(items2)
"status3":
setListContent(items3)
else
setListContent(items1)
}
}
private fun setListContent(itemsList: List<?>) {
myListView.adapter = MyCustomadapter(context!!, itemsList)
}
}