I'm updating my knowledge of Android, and I want to make an app with a Drawer menu, call an api and display the values inside a fragment. Starting from the template created by android studio itself, I have followed this tutorial:https://howtodoandroid.com/mvvm-retrofit-recyclerview-kotlin/ but I have a problem when programming the MainActivity.
Android studio template create this fragment (only changes the name of fragments):
class CheckListFragment : Fragment() {
private var _binding: FragmentCheckListBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val checklistViewModel =
ViewModelProvider(this).get(CheckListViewModel::class.java)
_binding = FragmentCheckListBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textChecklist
checklistViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
The issue is that I don't know how I should call the viewmodel in that section, since I've tried in different ways, reading and reading examples, but none is exactly what I need. If you need me to show more parts of the code (viewmodel, viewmodelfactory etc. I can add it without any problem, although they are practically the same as those described in the tutorial)
In that example, in the MainActivity class we have this:
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var binding: ActivityMainBinding
lateinit var viewModel: MainViewModel
private val retrofitService = RetrofitService.getInstance()
val adapter = MainAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
viewModel = ViewModelProvider(this, MyViewModelFactory(MainRepository(retrofitService))).get(MainViewModel::class.java)
binding.recyclerview.adapter = adapter
viewModel.movieList.observe(this, Observer {
Log.d(TAG, "onCreate: $it")
adapter.setMovieList(it)
})
viewModel.errorMessage.observe(this, Observer {
})
viewModel.getAllMovies()
}
But here not use fragments.
I would greatly appreciate help, or a link where I can see an example of this
Related
I have two fragments that share information with each other, in the first one I have an edit text and button widget. The second fragment is just a listview. When the user clicks the button, it displays whatever is in the edit text widget in the second fragment.
So if the user enters the text study and clicks the button the second fragment will display
Study
If the user then enters the text eat and clicks the button, the second fragment will display
Study
Eat
I am having so issues with displaying the texts
So far this is what I have done
class FirstFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.one_fragment, container, false)
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
}
return view;
}
}
class SecondFragment : Fragment() {
lateinit var viewModel: MyViewModel
#SuppressLint("MissingInflatedId")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
viewModel = activity?.run { ViewModelProvider(this)[MyViewModel::class.java]
} ?: throw Exception("Invalid Activity")
val view = inflater.inflate(R.layout.page3_fragment, container, false)
val valueView = v.findViewById<TextView>(R.id.textView)
return view
The problem I am having is how to display the texts
If I undestand you correctly, you want to share data between fragments? If yes, you can do that with "shared" viewModel. For example:
class FirstFragment : Fragment() {
private var _binding: FragmentFirstBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentFirstBinding.inflate(inflater, container, false)
binding.buttonChangeFragment.setOnClickListener {
/*
You can change data here, or in navigateWithNavController() from
activity (You already have an instance of your viewModel in activity)
*/
sharedViewModel.changeData(binding.myEditText.text.toString())
if (requireActivity() is YourActivity)
(requireActivity() as YourActivity).navigateWithNavController()
}
return binding.root
}
}
class SecondFragment : Fragment() {
private var _binding: FragmentSecondBinding? = null
private val binding get() = _binding!!
private val sharedViewModel by activityViewModels<SharedViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSecondBinding.inflate(inflater, container, false)
binding.secondFragmentText.text = sharedViewModel.someData.value
return binding.root
}
}
and your activity:
class YourActivity: AppCompatActivity() {
private lateinit var binding: YourActivityBinding
private lateinit var appBarConfiguration: AppBarConfiguration
private val sharedViewModel: SharedViewModel by lazy {
ViewModelProvider(
this
)[SharedViewModel::class.java]
}
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = YourActivityBinding.inflate(LayoutInflater.from(this))
setContentView(binding.root)
navController = this.findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(navController.graph)
}
/*
This function is just for test
*/
fun navigateWithNavController() {
navController.navigate(R.id.secondFragment)
}
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, appBarConfiguration)
}
}
And your viewModel should look something like this:
class SharedViewModel : ViewModel() {
private val _someData = MutableLiveData("")
val someData: LiveData<String>
get() = _someData
fun changeData(newData: String?) {
_someData.value = newData ?: _someData.value
}
}
Your view model should have a backing list of the entered words. When a word is added, the list can be updated, and in turn you can update a LiveData that publishes the latest version of the list.
class MyViewModel: ViewModel() {
private val backingEntryList = mutableListOf<String>()
private val _entryListLiveData = MutableLiveData("")
val entryListLiveData : LiveData<String> get() = _entryListLiveData
fun addEntry(word: String) {
backingEntryList += word
_entryListLiveData.value = backingEntryList.toList() // use toList() to to get a safe copy
}
}
Your way of creating the shared view model is the hard way. The easy way is by using by activityViewModels().
I also suggest using the Fragment constructor that takes a layout argument, and then setting things up in onViewCreated instead of onCreateView. It's less boilerplate code to accomplish the same thing.
In the first fragment, you can add words when the button's clicked:
class FirstFragment : Fragment(R.layout.one_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val button = view.findViewById<Button>(R.id.vbutton)
val value = view.findViewById<EditText>(R.id.textView)
button.setOnClickListener {
viewModel.addEntry(value.text.toString())
}
}
}
In the second fragment, you observe the live data:
class SecondFragment : Fragment(R.layout.page3_fragment) {
private val viewModel by activityViewModels<MyViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val valueView = view.findViewById<TextView>(R.id.textView)
viewModel.entryListLiveData.observe(viewLifecycleOwner) { entryList ->
valueView.text = entryList.joinToString(" ")
}
}
}
Is there a more generic way to init this two initialization lines?
private var _binding: MyFragmentViewBinding? = null
private val binding get() = _binding!!
Should we call every time binding
binding.cancelButton.setOnClickListener { }
binding.homeButton.setOnClickListener { }
binding.aboutButton.setOnClickListener { }
Or to create class variable?
cancelButton = binding.cancelButton
binding.cancelButton.setOnClickListener{}
And, should we set binding = null in adapter?
I think it is more of a personal preference. I like to do it with extension and higher-order function
fun <T : ViewDataBinding> Fragment.getDataBinding(layout: Int, container: ViewGroup?): T {
val binding: T = DataBindingUtil.inflate(layoutInflater, layout, container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding
}
My Fragment looks like this.
class InviteFragment : Fragment() {
private lateinit var binding: FragmentInviteBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = getDataBinding(R.layout.fragment_invite, container)
return binding.apply {
inviteAll.setOnClickListener(onInviteAllClick)
// You can set as many click listener here
// or some initialization related to view such as
// setting up recycler view adapter and decorators
}.root
}
private val onInviteAllClick = View.OnClickListener {
// Invite users
}
}
By doing things like this your onCreateView will be more readable and never going to get very long.
Please help me with a problem.
I have the activity_main.xml set up with a score text box, a test button and a fragment container that swaps between 2 fragments.
The fragments are basically containers for buttons.
The "buttonTest" does exactly what "button1" from the fragment does (increments the score) but the test button (located on activity_main.xml) works and the fragment one ... does not
When viewing the logs ...i see that score does update when i click both buttons but with different values.
If i click "buttonTest" it adds 5 to score so score = 5. On another click it adds another 5 so score = 10
If i click in the fragment on "button1" ... calling the same method score is now 1 then 2 then 3.
If i now click "buttonTest" the score is 15.
The problem is that the LiveData keeps separate values depending on where the method was called.
Allso the MainActivity observer does not update on the fragment call... only on the activity one.
Please help.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val scoreSwitch = binding.scoreSwitch
binding.buttonTest.setOnClickListener { viewModel.adaugaTren(5) }
// score observer
viewModel.scor.observe(this, Observer { newScore ->
binding.txtPunctaj.text = newScore.toString()
//todo sterge LOG
Log.d("test","Scor Observer triggers")
})
.......
class MainViewModel: ViewModel(){
private val _score = MutableLiveData(0)
val score: LiveData<Int>
get() = _score
fun adaugaTren(valoare:Int) {
_scor.value = _scor.value?.plus(valoare)
listaTrenuri.add(valoare)
//testing
Log.d("test","Trenuri: ${scor.value}")
updateDisplay()
}
fun updateDisplay(){
// _trenuriDetinute.value = listaTrenuri.toString()
_trenuriDetinute.value = TextUtils.join(", ",listaTrenuri)
//testing
Log.d("test","UpdateDisplay: ${scor.value}")
}
...
}
class TrenuriFragment : Fragment() {
private lateinit var binding: FragmentTrenuriBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
binding = DataBindingUtil.inflate(inflater,R.layout.fragment_trenuri, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initButtons()
}
fun initButtons(){
binding.button1.setOnClickListener { viewModel.adaugaTren(1)}
....
}
}
In TrenuriFragment use activityViewModels
// viewModels is scoped in fragment but you need activity
private val viewModel: MainViewModel by activityViewModels()
I am currently working on my first Android app using Kotlin. In my activity are a lot of UI elements which I use to show dynamic information (see example below). For performance reasons I learned:
"Define a variable in the class and initialize it in the onCreate()
method."
This is kind of messy and my question is: are there other techniques to fulfill the same task but have a cleaner code? The variables are used in other methods later.
class MainActivity : AppCompatActivity() {
private lateinit var text_1: TextView
private lateinit var text_2: TextView
private lateinit var text_3: TextView
private lateinit var text_4: TextView
[...]
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
text_1 = findViewById(R.id.text1)
text_2 = findViewById(R.id.text2)
text_3 = findViewById(R.id.text3)
text_4 = findViewById(R.id.text4)
[...]
}
From ViewBinding official docs:
View binding is a feature that allows you to more easily write code that interacts with views
First, enable ViewBinding in your module:
android {
...
buildFeatures {
viewBinding true
}
}
Then, if you're calling views from activity, you should:
private lateinit var binding: ResultProfileBinding
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}
and then you use binding instance to call the views:
binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }
If you are calling views from a fragment, you should do it like following to avoid leaks:
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
In Kotlin you just need to use the id directly without binding. The class will import this:
import kotlinx.android.synthetic.main.<your_layout_xml>.*
In this case it will import: kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
text_1.text = "Text1"
text_2.text = "Text2"
[...]
}
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
}