I am trying to develop a Survey application for Android. In the requirements, the client is asking for a swipe effect between the questions. I used a ViewPager with FragmentPagerAdapter and everything works fine.
The problem comes when they require a Tree Decision System. In other words, when the surveyed person select an answer, I should lead him to a question according on which one is defined in that answer.
As you can see in the image, before answer the first question, I can't load the second page because I don't know which will be the question to load.
Also I can't know the number of pages that should I return in the getCount method, only when the user responds, I can know if there's one more page or not, and which should be its content.
I tried many solution posted over there, but the most important, or at least was logic for me. Is to set the count as the known pages number, and when the user select an answer, I tried to change the count and call notifyDataSetChanged method, but all what I get, is to change the number of pages dynamically, but not the content.
The scenario is:
In the first question, I set the count to 1, so the user can't swipe to the next page because it's unknown.
When the user select an answer, I change the count to 2 and load the next question. Everything OK!
If the user back to the first question and change his answer, I tried to destroy or change the content of the second page. But in this case, the notifyDataSetChanged doesn't replace the Fragment.
I know that I am asking a strange and difficult behavior, but I want to know if someone has to do the same thing and could find the right solution.
Sorry for don't post any code, but after so many intents, my code becomes ugly and I do revert in VCS.
You could try a linked list for your data items. Here's a quick example of how that might look.
var viewPager: ViewPager? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_dynamic_view_pager, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.viewPager)
viewPager?.apply {
adapter = MyAdapter(childFragmentManager, this).also {
it.initialDataItem = buildDataItems()
}
}
}
fun buildDataItems(): DataItem {
val item0 = DataItem(0).also {
it.title = "What can I help you with today?"
}
val item1 = DataItem(1).also {
it.title = "Technical Problem"
}
val item2 = DataItem(2).also {
it.title = "Ordering Problem"
}
val item3 = DataItem(3).also {
it.title = "Is your power light on?"
}
val item4 = DataItem(4).also {
it.title = "Have you received your order?"
}
val item5 = DataItem(5).also {
it.title = "New content node"
}
val item6 = DataItem(6).also {
it.title = "New content node"
}
item0.yesItem = item1
item0.noItem = item2
item1.yesItem = item3
item2.yesItem = item4
item3.yesItem = item5
item3.noItem = item6
return item0
}
data class DataItem(
var id: Int = 0
) {
var title: String = ""
var yesItem: DataItem? = null
var noItem: DataItem? = null
var answer: Answer = Answer.UNANSWERED
}
enum class Answer {
UNANSWERED,
YES,
NO
}
class MyAdapter(fm: FragmentManager, val viewPager: ViewPager) : FragmentPagerAdapter(fm) {
var initialDataItem: DataItem = DataItem()
override fun getItem(position: Int): Fragment {
var index = 0
var dataItem: DataItem? = initialDataItem
while (index < position) {
when (dataItem?.answer) {
Answer.YES -> dataItem = dataItem.yesItem
Answer.NO -> dataItem = dataItem.noItem
else -> {}
}
index ++
}
return DetailFragment(dataItem) {
dataItem?.answer = if (it) Answer.YES else Answer.NO
notifyDataSetChanged()
viewPager.setCurrentItem(position + 1, true)
}
}
override fun getCount(): Int {
var count = 1
var dataItem: DataItem? = initialDataItem
while (dataItem?.answer != Answer.UNANSWERED) {
when (dataItem?.answer) {
Answer.YES -> dataItem = dataItem.yesItem
Answer.NO -> dataItem = dataItem.noItem
else -> {}
}
count++
}
return count
}
}
class DetailFragment(val dataItem: DataItem?, val listener: ((answeredYes: Boolean) -> Unit)) : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_viewpager_detail, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<TextView>(R.id.title)?.text = dataItem?.title
view.findViewById<Button>(R.id.yesButton)?.setOnClickListener {
listener(true)
}
view.findViewById<Button>(R.id.noButton)?.setOnClickListener {
listener(false)
}
}
}
Note: You will want to refine this a bit to handle error cases and reaching the end of the tree. Also, you'll want to use a better method for injecting data into the detail fragment - this is just for illustration purposes.
Related
I am training with a simple app to show movies, I use an MVVM pattern and Flow.
Problem
This is my home, filterable through chips
I click on a movie , the details screen comes up then I go back to the home and this is the result:
Using logcat the home screen gets the list of movies to show but is not shown in the recyclerview (which uses diffUtil).
Below is the code for my fragment:
#AndroidEntryPoint
class Home2Fragment : Fragment() {
private val TAG = Home2Fragment::class.simpleName
private var _binding: FragmentHome2Binding? = null
private val binding: FragmentHome2Binding
get() = _binding!!
private val viewModel: HomeViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHome2Binding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
initChipGroupSpecificMovieList()
val adapter = MovieAdapter()
sectionRv.setHasFixedSize(true)
sectionRv.adapter = adapter
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.movieListBySpecification.collectLatest {
Log.d(TAG, "onViewCreated: received list")
adapter.addItems(it)
}
}
}
}
}
private fun FragmentHome2Binding.initChipGroupSpecificMovieList() {
val sortByMap = HomeViewModel.Companion.MovieListSpecification.values()
chipGroup.removeAllViews()
for (specification in sortByMap) {
val chip = Chip(context)
chip.isCheckable = true
chip.id = specification.ordinal
chip.text = getString(specification.nameResource)
chip.setOnCheckedChangeListener { _, isChecked ->
if (isChecked)
viewModel.setMovieListSpecification(specification)
}
chipGroup.addView(chip)
}
chipGroup.check(sortByMap.lastIndex - sortByMap.size + 1)//check first element
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
it seems at the line of code where I try to insert the list of movies in the adapter this doesn't add them because maybe via diffUtil it finds that it is the previous list and so it doesn't load it. However it doesn't show the previous one either, possible solutions?
as java code you can use this
#Override
public void onResume() {
super.onResume();
if(it.size() > 0) {
adapter.addItems(it)
}
}
Get numbers
class Base : Fragment() {
val time = ArrayList<Double>()
val amplitude = ArrayList<Double>()
var flag = 0
private fun readNumbersFromCSV(fileName: String) {
val textView: TextView = requireView().findViewById(R.id.result)
val timeTextView: TextView = requireView().findViewById(R.id.Time)
val amplitudeTextView: TextView = requireView().findViewById(R.id.Amplitude)
timeTextView.movementMethod = ScrollingMovementMethod()
amplitudeTextView.movementMethod = ScrollingMovementMethod()
try {
timeTextView.append("Time, s\n")
amplitudeTextView.append("Amplitude\n")
val file = File(fileName)
if(!file.exists()){
throw FileNotFoundException("File not found")
}
val reader = BufferedReader(FileReader(file))
var line = reader.readLine()
while (line != null) {
val parts = line.split(",")
if (parts.size == 2) {
time.add(parts[1].toDouble())
amplitude.add(parts[0].toDouble())
timeTextView.append(parts[1] + "\n")
amplitudeTextView.append(parts[0] + "\n")
}
line = reader.readLine()
}
flag = 1
reader.close()
} catch (e: FileNotFoundException) {
textView.text = "Error: File Not Found"
} catch (e: Exception) {
textView.text = "Error: ${e.message}"
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_base, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()
val file = File(path, "data.csv").toString()
readNumbersFromCSV(file)
/*now im ready to pass data to another class*/
}
}
Do some calculations on those numbers
class Calculations : Fragment() {
private fun meanAmplitude(amplitudes: List<Double>): Double {
if(amplitudes.isEmpty()) return 3.5
return amplitudes.sum() / amplitudes.size
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_calculations, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val copiedList = Base().amplitude.toList() /* data from file passed to new array*/
val textViewAmp: TextView = view.findViewById(R.id.Camplitude)
val valueOfMean = meanAmplitude(copiedList).toString() /*calculate mean value*/
textViewAmp.text = valueOfMean /*display it*/
}
}
MyAdapter
internal class MyAdapter (var context: Context, fm: FragmentManager, var totalTabs: Int): FragmentPagerAdapter(fm) {
override fun getCount(): Int {
return totalTabs
}
override fun getItem(position: Int): Fragment {
return when(position){
0 -> {
Base()
}
1 -> {
Calculations()
}
2 -> {
About()
}
else -> getItem(position)
}
}
}
HomeActivity
class HomeActivity : AppCompatActivity() {
private lateinit var tabLayout: TabLayout
private lateinit var viewPager: ViewPager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
supportActionBar?.hide()
setContentView(R.layout.activity_home)
tabLayout = findViewById(R.id.tabLayout)
viewPager = findViewById(R.id.viewPager)
tabLayout.addTab(tabLayout.newTab().setText("Data"))
tabLayout.addTab(tabLayout.newTab().setText("Calculations"))
tabLayout.addTab(tabLayout.newTab().setText("About"))
tabLayout.tabGravity = TabLayout.GRAVITY_FILL
val adapter = MyAdapter(this, supportFragmentManager, tabLayout.tabCount)
viewPager.adapter = adapter
viewPager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout))
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
viewPager.currentItem = tab!!.position
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
}
}
Im new in Kotlin. I have a problem with initializing an array that is being filled with data from a .csv file in the Base class, and then its contents should be passed to the Calculations class. The problem is that the array instance is being passed before it is being filled with numbers. Two fragments are generated probably in the same time.
Loading from file and initializing an array in the first class works, elements are displayed on the screen without any problems. After passing the array to the second class, it is empty.
I tried to do a flag, but it doesnt work like I though. Im not using activities, just Fragments and ViewPager. I tried Bundles but its hard to apply new things in my messy project.
Here:
val copiedList = Base().amplitude.toList()
You are instantiating a new instance of Base by calling its constructor. This new instance shares nothing with any previous instance. It's a brand new Base that hasn't done anything yet so its lists are still empty.
To pass data between fragments, you should create an arguments Bundle and pass that to the new fragment. The reason you need to do it this way is that Android automatically destroys and recreates Fragment instances under various conditions, and only the arguments data is preserved for the new instance.
The conventional way to do this is to define a Fragment factory function named newInstance() in its companion object. Then the Fragment can unpack the new data in onViewCreated(). You have to convert to and from DoubleArrays because Bundle doesn't support Lists.
class Calculations private constructor(): Fragment(R.layout.fragment_calculations) {
companion object {
private const val TIME_LIST_KEY = "timeList"
private const val AMP_LIST_KEY = "ampList"
fun newInstance(timeList: List<Double>, ampList: List<Double>) =
Calculations().apply {
arguments = bundleOf(
TIME_LIST_KEY to timeList.toDoubleArray(),
AMP_LIST_KEY to ampList.toDoubleArray()
)
}
}
private fun meanAmplitude(amplitudes: List<Double>): Double {
if(amplitudes.isEmpty()) return 3.5
return amplitudes.sum() / amplitudes.size
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val timeList = requireArguments().getDoubleArray(TIME_LIST_KEY).toList()
val ampList = requireArguments().getDoubleArray(AMP_LIST_KEY).toList()
val textViewAmp: TextView = view.findViewById(R.id.Camplitude)
val valueOfMean = meanAmplitude(ampList).toString() /*calculate mean value*/
textViewAmp.text = valueOfMean /*display it*/
}
}
Then in your first fragment, you use Calculations.newInstance() to create your second fragment before passing it to the transaction manager.
By the way, there's a major bug in your Base class. Since Fragment instances can be reused by the OS, the same fragment can go through multiple lifecycles. Since you are adding your data to the same ArrayLists every time onViewCreated() is called, they will get longer and longer as the user rotates the screen or navigates back and forth in the app. You should either remove those properties and use local variables instead, or you should clear those ArrayLists in onDestroyView().
I'm new to Android
And I'm having a problem with displaying the list items in the RecyclerView.
I was hoping to display the list one by one and append while it is still loading.
But what happens is, the list will appear after all the items are fetched.
Here is my Fragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_lock_list, container, false)
val recyclerView = view.findViewById<RecyclerView>(R.id.list)
for(device in devices) {
fetchBuildingAddress(device)
}
if (recyclerView is RecyclerView) {
with(recyclerView) {
layoutManager = LinearLayoutManager(context)
adapter = deviceRecyclerViewAdapter
}
}
return view
}
private fun fetchBuildingAddress(device: Device) {
deviceWearableService.getBuilding(device.buildingId) {
when(it) {
is DeviceWearableServiceImpl.State.Success -> {
val buildingName = it.resources.buildingAddress.address1
val deviceBuilding = DeviceBuillding(device, buildingName)
deviceRecyclerViewAdapter.insertDevice(deviceBuilding)
}
else -> {
// TODO: ERROR STATE
}
}
}
}
And in my insertDevice
I simply call the notifyItemInserted in the adapter.
fun insertDevice(updateDeviceBuilding: DeviceBuillding) {
deviceBuilding.add(updateDeviceBuilding)
notifyItemInserted(deviceBuilding.size - 1 )
}
You could also suggest what is the better behavior! Thank you so much in advance.
It's unclear that you are fetching paged data or complete data , but what you need is to use flow with a Diff callback in your adapter i.e PagingDataAdapter(example) or ListAdatper(example).
Its implementation is quite easy you can find it on net.
I have a list in "All Users" Fragment. When a name on list is clicked, It should switch to "User Details" Fragment. I can't seem to find a way to somehow destroy this fragment and bring up the new one, or just display the new one on top of old.
The language used is Kotlin. I'm new to this. I tried some ways with Fragment Manager but can't seem to figure out the right syntax.
Main Activity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Default view when app starts
title = resources.getString(R.string.allusers)
loadFragment(AllUsersFragment())
//Bottom Navigation Bar
navigationView.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.navigation_allusers -> {
title = resources.getString(R.string.allusers)
loadFragment(AllUsersFragment())
return#setOnNavigationItemSelectedListener true
}
R.id.navigation_transfer -> {
title = resources.getString(R.string.transfer)
loadFragment(TransferFragment())
return#setOnNavigationItemSelectedListener true
}
R.id.navigation_logs -> {
title = resources.getString(R.string.logs)
loadFragment(LogsFragment())
return#setOnNavigationItemSelectedListener true
}
}
false
}
}
//function to load fragment when bottom navigation is clicked
fun loadFragment(fragment: Fragment) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.container, fragment)
transaction.addToBackStack(null)
transaction.commit()
}
}
AllUsersFragment.kt
class AllUsersFragment : Fragment(){
private lateinit var listView:ListView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val rootView = inflater.inflate(R.layout.fragment_allusers, container, false)
val details = UserDetailsFragment()
val bundle = Bundle() //To transfer information
listView = rootView.findViewById(R.id.usersList)
val userNames = arrayOf(
"Andrew Jackson",
"Barry Alan",
"Caitlyn Snow",
"Drake Ramoray"
)
val adapter = ArrayAdapter(context, android.R.layout.simple_list_item_1, userNames)
listView.adapter = adapter
//List view item clicked listener
listView.onItemClickListener = AdapterView.OnItemClickListener { adapterView, view, i, l ->
val userName = userNames[i]
bundle.putString("str", userName)
bundle.putInt("int", i)
details.arguments = bundle
}
return rootView
}
}
UserDetails.kt
class UserDetailsFragment : Fragment(){
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val rootView = inflater.inflate(R.layout.fragment_user_details,container,false)
val nameField = rootView.findViewById<TextView>(R.id.nameField)
val imageField = rootView.findViewById<ImageView>(R.id.imageField)
//Importing images
val andrewjackson = BitmapFactory.decodeResource(context!!.resources,R.drawable.andrewjackson)
val barryalan = BitmapFactory.decodeResource(context!!.resources,R.drawable.barryalan)
val caitlynsnow = BitmapFactory.decodeResource(context!!.resources,R.drawable.caitlynsnow)
val drakeramoray = BitmapFactory.decodeResource(context!!.resources,R.drawable.drakeramoray)
val userImages = ArrayList<Bitmap>()
userImages.add(andrewjackson)
userImages.add(barryalan)
userImages.add(caitlynsnow)
userImages.add(drakeramoray)
val name = arguments!!.getString("str") //Displays name clicked on Details Page
nameField.setText(name)
val i =arguments!!.getInt("int") //Displays image of user from array
imageField.setImageBitmap(userImages[i])
return rootView
}
}
I think using the loadFragment function in MainActivity.kt it might be possible, but I can't figure out a way to use that inside the fragment.
Expected Output:
When a name on the list is clicked, it should change to another fragment in the same container, that displays user details.
You can use this code in your fragment to replace the fragment in your container
val someFragment = YourUserDetailsFragment()
val transaction = fragmentManager!!.beginTransaction()
transaction.replace(R.id.container, someFragment)
transaction.addToBackStack(null)
transaction.commit()
Yes, There are two ways using Interface and by the public method. As your fragment is bound with the activity that means it's the parent is activity.
So you can access the method just to make public inactivity and access by using
(activity as MainActivity).loadFragment(//Your fragment intance)
Here is the example to communicate between activity and fragments.
You already have everything set up, in your, on click listener you should just call activity?.loadFragment(UserDetailsFragment())
listView.onItemClickListener = AdapterView.OnItemClickListener { adapterView, view, i, l ->
val userName = userNames[i]
bundle.putString("str", userName)
bundle.putInt("int", i)
details.arguments = bundle
(activity as MainActivity)?.loadFragment(details)
}
I have set certain condition about when to fetch data from the internet, if the last time fetching data is more than 10 minutes ago, then fetch data from the internet, So i don't need to fetch data over and over again when coming back from other fragment. I wrote this code inonResume ,
I assume the product data will still be on my RecyclerView after coming back from other fragment.
If the last time fetching data is more than 10 minutes ago, then I can populate the RecyclerView view with the product data like this :-
But problem is, when i move from Home Fragment to other fragment, For example if tap other tab in the bottom navigation menu, RecyclerView seems empty, it just text view that appear on the screen like this. (If I back again to the home fragment, it means the last time I fetch product data from server is not more than 10 minutes ago)
the toolbar and the bottom navigation are part of my Main Activity, so I change fragment in the center part
is my problem because of the onDestroy and onDetach of my HomeFragment is activated when I change to other fragment ?
what went wrong in here ?
here is the simplified code of my Home Fragment
class HomeFragment : androidx.fragment.app.Fragment() {
lateinit var mContext : Context
lateinit var mActivity : FragmentActivity
lateinit var recyclerView1 : RecyclerView
lateinit var fragmentView : View
private var firstProducts = listOf<Product>()
lateinit var firstProductAdapter : ProductListAdapter
override fun onAttach(context: Context) {
super.onAttach(context)
mContext = context
activity?.let { mActivity = it }
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// set up view
fragmentView = inflater.inflate(R.layout.fragment_home, container, false)
recyclerView1 = fragmentView.findViewById(R.id.recyclerView_1)
return fragmentView
}
override fun onResume() {
super.onResume()
if (lastTimeFetchDataIsMoreThan10MinutesAgo) {
getProducts() // when I open the app for the very first time, I fetch the product data
} else {
// when it is more than 10 minutes, do nothing
}
}
override fun onStop() {
super.onStop()
progressBar.visibility = View.INVISIBLE // to ensure the progress bar will always dissapear if move to another destination
}
private fun getProducts(type: String) {
showProgressBar(true)
Product.getProductsFromServer(customerID = userData.id.toString(), type = type) { errorMessage, products ->
errorMessage?.let {
activity?.toast(it)
} ?: run {
val productList = products ?: ArrayList()
setUpRecyclerView(type = type,products = productList)
}
}
showProgressBar(false)
}
private fun setUpRecyclerView(type: String, products: List<Product>) {
val productAdapter = ProductListAdapter(context = mContext,products = products)
val layoutManager = LinearLayoutManager(mContext,LinearLayoutManager.HORIZONTAL,false)
if (type == "special") {
firstProductAdapter = productAdapter
firstProducts = products
recyclerView1.adapter = productAdapter
recyclerView1.layoutManager = layoutManager
recyclerView1.setHasFixedSize(true)
}
}
private fun showProgressBar(enable: Boolean) {
if (enable) {
progressBar.visibility = View.VISIBLE
recyclerView1.visibility = View.GONE
selectedProductTextView.visibility = View.GONE
bestSellingProductTextView.visibility = View.GONE
} else {
progressBar.visibility = View.GONE
recyclerView1.visibility = View.VISIBLE
selectedProductTextView.visibility = View.VISIBLE
bestSellingProductTextView.visibility = View.VISIBLE
}
}
}