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
}
Related
I want use one viewModel and observe object from one method. But I dont want write this observe method in all fragment. Only write in one place and use other fragments. I think I need fragment extension but can't get it how do this. I need help.
This viewModel that I want use.
SharedViewModel.kt
class SharedViewModel #Inject constructor(private val notificationServiceRepo: NotificationServiceRepo) : ViewModel() {
private val _helpNotification = SingleLiveEvent<NetworkResult<BaseResponse<Any>>>()
val helpNotification get() = _helpNotification
fun postHelpNotification(helpNotificationRequest: HelpNotificationRequest) = viewModelScope.launch (
Dispatchers.IO){
_helpNotification.postValue(NetworkResult.Loading)
_helpNotification.postValue(notificationServiceRepo.postNotificationHelp(helpNotificationRequest))
}
}
this is call method and observe function:
MainFragment.kt
viewModel.postHelpNotification(HelpNotificationRequest(0))
viewModel.helpNotification.observe(viewLifecycleOwner) {
it?.onLoading {}
it?.onSuccess { result->
result?.let {
InfoDialogWithOneText(
InfoDialogType.GOT_HELP_ASKING_FROM_STAFF
).show(childFragmentManager, InfoDialog.TAG)
}
}
it?.onError { error ->
InfoDialogWithOneText(
InfoDialogType.GOT_HELP_ASKING_FROM_STAFF
).show(childFragmentManager, InfoDialog.TAG)
}
}
}
Tried use sharedViewModel but I will have to write observe method for all.
Tried to baseViewModel but it get error hilt view model and also it will be same like shared view model.
For the abstract part, you want it need to cover everything - from having a view model that provides the observable event and handling it. I had to change some types because you did not provide them but it should not be too difficult to apply this to your case.
abstract class HelpNotificationFragment : Fragment() {
internal abstract val viewModel: HelpNotificationViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.helpNotification.observe(viewLifecycleOwner) { result ->
println(result)
}
}
}
abstract class HelpNotificationViewModel : ViewModel() {
private val _helpNotification = SingleLiveEvent<Result<Any>>()
// Specify the immutable type otherwise you would expose it as mutable
val helpNotification: LiveData<Result<Any>>
get() = _helpNotification
fun postHelpNotification(helpNotificationRequest: Result<Any>) {
viewModelScope.launch(Dispatchers.IO) {
_helpNotification.postValue(helpNotificationRequest)
}
}
}
And this is how you would implement the fragments that would use it - the overriding the view model will take care of forcing you to use view model with proper parent:
#HiltViewModel
class MainViewModel #Inject constructor(): HelpNotificationViewModel()
#AndroidEntryPoint
class MainFragment : HelpNotificationFragment() {
override val viewModel: MainViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_main, container, false)
view.findViewById<Button>(R.id.button).setOnClickListener {
viewModel.postHelpNotification(Result.success("Yay!"))
}
return view
}
}
Well I am a beginner with android and kotlin so I have been trying to send a variable semesterSelected from the fragment ViewCourses to my viewmodel UserViewModel is the codes are down below.
`class ViewCourses(path: String) : ReplaceFragment() {
private var semesterSelected= path
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
container?.removeAllViews()
return inflater.inflate(R.layout.fragment_view_courses, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
userRecyclerView = view.findViewById(R.id.recyclerView)
userRecyclerView.layoutManager = LinearLayoutManager(context)
userRecyclerView.setHasFixedSize(true)
adapter = MyAdapter()
userRecyclerView.adapter = adapter
makeToast(semesterSelected)
// The variable I am trying to send to UserViewModel is -->> semesterSelected
var viewModel: UserViewModel = ViewModelProvider(this)[UserViewModel::class.java]
viewModel.allUsers.observe(viewLifecycleOwner) {
adapter.updateUserList(it)
}
}
}
class UserViewModel : ViewModel() {
private val repository: UserRepository = UserRepository("CSE/year3semester1").getInstance()
private val _allUsers = MutableLiveData<List<CourseData>>()
val allUsers: LiveData<List<CourseData>> = _allUsers
init {
repository.loadUsers(_allUsers)
}
}
The reason I am doing this is I am wanting a to send a variable to my repository UserRepository all the way from ViewCourses and thought sending this via UserViewModel might be a way .
class UserRepository(semesterSelected: String) {
// The variable I am expecting to get from UserViewModel
private var semesterSelected = semesterSelected
private val databaseReference: DatabaseReference =
FirebaseDatabase.getInstance().getReference("course-list/$semesterSelected")
#Volatile
private var INSTANCE: UserRepository? = null
fun getInstance(): UserRepository {
return INSTANCE ?: synchronized(this) {
val instance = UserRepository(semesterSelected)
INSTANCE = instance
instance
}
}
fun loadUsers(userList: MutableLiveData<List<CourseData>>) {
databaseReference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
try {
val courseList: List<CourseData> = snapshot.children.map { dataSnapshot ->
dataSnapshot.getValue(CourseData::class.java)!!
}
userList.postValue(courseList)
} catch (e: Exception) {
}
}
override fun onCancelled(error: DatabaseError) {
}
})
}
}
I tried something like below
class ViewCourses(path: String) : ReplaceFragment() {
private var semesterSelected= path
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
container?.removeAllViews()
return inflater.inflate(R.layout.fragment_view_courses, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
userRecyclerView = view.findViewById(R.id.recyclerView)
userRecyclerView.layoutManager = LinearLayoutManager(context)
userRecyclerView.setHasFixedSize(true)
adapter = MyAdapter()
userRecyclerView.adapter = adapter
makeToast(semesterSelected)
**// Sending the variable as parameter**
var viewModel: UserViewModel = ViewModelProvider(this)[UserViewModel(semesterSelected)::class.java]
viewModel.allUsers.observe(viewLifecycleOwner) {
adapter.updateUserList(it)
}
}
}
class UserViewModel(semesterSelected: String) : ViewModel() {
private val repository: UserRepository = UserRepository("CSE/year3semester1").getInstance()
private val _allUsers = MutableLiveData<List<CourseData>>()
val allUsers: LiveData<List<CourseData>> = _allUsers
init {
repository.loadUsers(_allUsers)
}
}
but doing this my app crashes . how can this be done ?
Thanks in Advance.
var viewModel: UserViewModel = ViewModelProvider(this)[UserViewModel(semesterSelected)::class.java]
UserViewModel(semesterSelected)::class.java NOR UserViewModel::class.java is a constructor for the view model.
If you would want to have ViewModel with that NEEDS initial parameters, you will have to create your own factory for that - which is a tad more complicated and for your case, it might be overkill for what you are trying to do but in the longterm it will pay off(Getting started with VM factories).
With that said, your needs can be easily solved by one function to initialize the view model.
class UserViewModel() : ViewModel() {
private lateinit var repository: UserRepository
private val _allUsers = MutableLiveData<List<CourseData>>()
val allUsers: LiveData<List<CourseData>> = _allUsers
fun initialize(semesterSelected: String) {
repository = UserRepository("CSE/year3semester1").getInstance()
repository.loadUsers(_allUsers)
}
}
A ViewModel must be created using a ViewModelProvider.Factory. But there is a default Factory that is automatically used if you don't specify one. The default factory can create ViewModels who have constructor signatures that are one of the following:
empty, for example MyViewModel: ViewModel.
saved state handle, for example MyViewModel(private val savedStateHandle: SavedStateHandle): ViewModel
application, for example MyViewModel(application: Application): AndroidViewModel(application)
both, for example MyViewModel(application: Application, private val savedStateHandle: SavedStateHandle): AndroidViewModel(application)
If your constructor doesn't match one of these four above, you must create a ViewModelProvider.Factory that can instantiate your ViewModel class and use that when specifying your ViewModelProvider. In Kotlin, you can use by viewModels() for easier syntax. All the instructions for how to create your ViewModelFactory are here.
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 am a newbie Android developer, and I am trying to observe a boolean set in the ViewModel from its parent's activity. I can observe its initial state as soon as the app launches, but any change applied later on doesn't seem to trigger the observer (i.e. when I switch the fragments).
Here is the code for my ViewModel:
class MyMusicViewModel : ViewModel() {
private var _MyMusicViewOn = MutableLiveData<Boolean>()
val MyMusicViewOn: LiveData<Boolean> get() = _MyMusicViewOn
init {
Timber.i("MyMusicViewModel Init Called!")
setMyMusicView(true)
}
override fun onCleared() {
super.onCleared()
Timber.i("MyMusicViewModel Cleared!")
setMyMusicView(false)
}
fun setMyMusicView(setter: Boolean) {
Timber.i("MyMusicViewModel setter called! %s", setter)
_MyMusicViewOn.value = setter
}
}
And here is its parent's activity:
class FullscreenActivity : AppCompatActivity() {
private val viewModel: MyMusicViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.MyMusicViewOn.observe(this, Observer { MyMusicViewOn ->
Timber.i("Observer called for MyMusicViewOn %s", MyMusicViewOn)
})
}
}
And in case you wanna see the ViewModel's related fragment, here it is:
class MyMusicFragment : Fragment() {
private lateinit var viewModel: MyMusicViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding = DataBindingUtil.inflate<FragmentMyMusicBinding>(
inflater,
R.layout.fragment_my_music,
container,
false
)
viewModel = ViewModelProvider(this).get(MyMusicViewModel::class.java)
return binding.root
}
override fun onResume() {
super.onResume()
Timber.i("MyMusicViewFragment resumed!")
viewModel.setMyMusicView(true)
}
}
What I am trying to accomplish is to observe the onResume(), onCleared() and init{} functions whenever they are called by changing the status of the MyMusicViewOn MutableLiveData Boolean. What I don't understand is why that boolean doesn't trigger the observer set in the parent activity whenever it changes.
Thankyou in advance for any thoughts!
All the best,
Fab.
I'm guessing that however you are populating that viewModel property in your Fragment, you are not using the Activity's ViewModel instance. The easiest way to get the same instance that the Activity is using would be to use the activityViewModels delegate:
private val viewModel: MyMusicViewModel by activityViewModels()
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.