Android: On BackPressed Fragment doesn't load content - android

so, I'm using the Android Navigation component inside my project. I have an Activity with 3 fragments that load perfectly and do everything they need to do. The problem is with one fragment that doesn't load its content when its returned to from backstack.
I'm using navigate functions declared in the ViewModel with Directions from the Navigation.
example ( vm.navigate(SomeFragmentDirections.actionSomeFragmentToOtherFragment)
A(activity) -> B(fragment) -> C(fragment) -> D(fragment)
when I press back on the D fragmentto go back to the C fragment it shows the upper navbar but doesn't load the content of it. I use the same principles on all other Activities/Fragments in my other projects (even in this one) and i don't get that problem. All lifecycle functions are called and everything should work fine. The logcat doesn't show any errors whatsoever. If anyone knows anything about this I would appreciate it.
EDIT:
This is the fragment that doesn't load (Fragment C)
Fragment D is the webView fragment, fragment C navigates to it in the
vm.navigate(RegisterFragmentDirections.actionRegisterFragmentToWebViewFragment(webURL)) function
class RegisterFragment : BaseFragment() {
private val vm: RegisterViewModel by viewModel()
override fun getViewModel(): BaseViewModel = vm
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentRegisterBinding.inflate(inflater, container, false)
context ?: return binding.root
injectFeature()
setToolbar(binding)
subscribeUi(binding)
return binding.root
}
/**
* set toolbar
* **/
private fun setToolbar(binding: FragmentRegisterBinding) {
if (activity is WelcomeActivity) {
binding.appBarLayout.backClickListener = (activity as WelcomeActivity).createOnBackClickListener()
} else if(activity is LoginActivity) {
binding.appBarLayout.backClickListener = (activity as LoginActivity).createOnBackClickListener()
}
}
/**
* set ui
* **/
private fun subscribeUi(binding: FragmentRegisterBinding) {
// set bindings
binding.contentRegister.viewModel = vm
binding.contentSuccess.viewOwner = this
// set true full screen
(activity as LoginActivity).setFullScreen(false)
// set dark status bar icons
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity!!.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
// set initial margin top
ViewCompat.setOnApplyWindowInsetsListener(binding.rootLayout) { _, insets ->
binding.appBarLayout.toolbar.setMarginTop(insets.systemWindowInsetTop)
insets
}
// set phone number mask listener
binding.contentRegister.etPhoneNumber.addTextChangedListener(PhoneNumberFormattingTextWatcher())
// set licence agreement formatted text with hyperlinks
setTextViewHTML(binding.contentRegister.licenceAgreement, getString(R.string.description_privacy_with_link))
// set listener on form elements, error handling
binding.contentRegister.etName.onFocusChangeListener = emptyInputValidationListener(
binding.contentRegister.etName,
binding.contentRegister.tilName,
getString(R.string.error_empty_name)
)
binding.contentRegister.etLastName.onFocusChangeListener = emptyInputValidationListener(
binding.contentRegister.etLastName,
binding.contentRegister.tilLastName,
getString(R.string.error_empty_last_name)
)
binding.contentRegister.etBirthDate.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
when(isBirthDateValid(binding.contentRegister.etBirthDate.text.toString())) {
false -> binding.contentRegister.tilBirthDate.error = getString(R.string.error_date_not_valid)
true -> binding.contentRegister.tilBirthDate.isErrorEnabled = false
}
}
}
binding.contentRegister.etEmail.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
when(!android.util.Patterns.EMAIL_ADDRESS.matcher(binding.contentRegister.etEmail.text!!.trim()).matches()) {
true -> binding.contentRegister.tilEmail.error = getString(R.string.error_email_not_valid)
false -> binding.contentRegister.tilEmail.isErrorEnabled = false
}
}
}
binding.contentRegister.etPassword.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
when (binding.contentRegister.etPassword.text!!.trim().length < 6) {
true -> binding.contentRegister.tilPassword.error =
getString(R.string.error_password_not_valid)
false -> binding.contentRegister.tilPassword.isErrorEnabled = false
}
}
}
binding.contentRegister.registerButton.setOnClickListener{
validateInputs(binding)
}
// set observables
vm.userResponse.observe(viewLifecycleOwner, Observer { updateRegisterSuccess(binding, it) })
}
/**
* update on success / failure
* **/
private fun updateRegisterSuccess(
binding: FragmentRegisterBinding,
resource: Resource<BaseResponseEntity>?
) {
resource?.let {
when (it.state) {
ResourceState.LOADING -> {
binding.contentProgress.isLoading = true
setViewAndChildrenEnabled(binding.rootLayout, false)
}
ResourceState.SUCCESS -> {
binding.contentProgress.isLoading = false
setViewAndChildrenEnabled(binding.rootLayout, true)
}
ResourceState.ERROR -> {
binding.contentProgress.isLoading = false
setViewAndChildrenEnabled(binding.rootLayout, true)
}
}
it.data?.let {
when(it.responseCode) {
RESPONSE_CODE_SUCCESS -> {
binding.contentSuccess.isSucceeded = true
setViewAndChildrenEnabled(binding.rootLayout, true)
}
RESPONSE_CODE_ERROR -> {
if (it.message.isNotEmpty()) {
showSnackbar(it.message, Snackbar.LENGTH_SHORT)
} else {
showSnackbar(getString(R.string.error_unknown), Snackbar.LENGTH_SHORT)
}
}
}
}
it.message?.let {
showSnackbar(getString(R.string.error_unknown), Snackbar.LENGTH_SHORT)
}
}
}
/**
* disable ui elements while loading
* **/
private fun setViewAndChildrenEnabled(view: View, enabled: Boolean) {
view.isEnabled = enabled
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
setViewAndChildrenEnabled(child, enabled)
}
}
}
/**
* validate all inputs
* **/
private fun validateInputs(binding: FragmentRegisterBinding) {
// check if all inputs are valid
if(binding.contentRegister.etName.text!!.trim().isEmpty()) {
binding.contentRegister.etName.requestFocus()
binding.contentRegister.tilName.error = getString(R.string.error_empty_name)
return
}
if(binding.contentRegister.etLastName.text!!.trim().isEmpty()) {
binding.contentRegister.etLastName.requestFocus()
binding.contentRegister.tilLastName.error = getString(R.string.error_empty_last_name)
return
}
if (binding.contentRegister.etBirthDate.rawText.isNotEmpty()) {
if (!isBirthDateValid(binding.contentRegister.etBirthDate.text.toString())) {
binding.contentRegister.etBirthDate.requestFocus()
binding.contentRegister.tilBirthDate.error =
getString(R.string.error_date_not_valid)
return
}
}
if(!android.util.Patterns.EMAIL_ADDRESS.matcher(binding.contentRegister.etEmail.text!!.trim()).matches()) {
binding.contentRegister.etEmail.requestFocus()
binding.contentRegister.tilEmail.error = getString(R.string.error_date_not_valid)
return
}
if(binding.contentRegister.etPassword.text!!.trim().length < PASSWORD_MINIMUM_LENGHT) {
binding.contentRegister.etPassword.requestFocus()
binding.contentRegister.tilPassword.error = getString(R.string.error_password_not_valid)
return
}
if(!binding.contentRegister.checkBox.isChecked) {
showSnackbar(getString(R.string.error_terms_and_conditions), Snackbar.LENGTH_SHORT)
return
}
// handle date of birth
val dateOfBirth = if (binding.contentRegister.etBirthDate.rawText.trim().isNotEmpty()
&& isBirthDateValid(binding.contentRegister.etBirthDate.rawText)) {
binding.contentRegister.etBirthDate.text.toString().replace("/", "-")
} else {
""
}
binding.rootLayout.hideKeyboard()
vm.register(
username = binding.contentRegister.etEmail.text.toString(),
password = binding.contentRegister.etPassword.text.toString(),
name = binding.contentRegister.etName.text.toString(),
lastName = binding.contentRegister.etLastName.text.toString(),
phoneNumber = binding.contentRegister.etPhoneNumber.text.toString(),
dateOfBirth = dateOfBirth)
Timber.d(dateOfBirth)
}
//todo handle this and move to util class
#Suppress("DEPRECATION")
private fun setTextViewHTML(text: TextView, html: String) {
// replace \n new line so android can show new line for text which we previously fetchCompanies from server
val hmtlFormatted = html.replace("\n", "<br>")
val sequence = Html.fromHtml(hmtlFormatted)
val strBuilder = SpannableStringBuilder(sequence)
val urls = strBuilder.getSpans(0, sequence.length, URLSpan::class.java)
for (span in urls) {
makeLinkClickable(strBuilder, span)
}
text.text = strBuilder
text.movementMethod = LinkMovementMethod.getInstance()
}
private fun makeLinkClickable(strBuilder: SpannableStringBuilder, span: URLSpan) {
val start = strBuilder.getSpanStart(span)
val end = strBuilder.getSpanEnd(span)
val flags = strBuilder.getSpanFlags(span)
val clickable = object : ClickableSpan() {
override fun onClick(view: View) {
// Do something with span.getURL() to handle the link click...
val webURL = span.url
vm.navigate(RegisterFragmentDirections.actionRegisterFragmentToWebViewFragment(webURL))
}
}
strBuilder.setSpan(clickable, start, end, flags)
strBuilder.removeSpan(span)
}
// PUBLIC ACTIONS ---
fun onRegisterDoneClick() {
// navigate to welcome activity and finish it
onRegisterSuccess()
}
/**
* on register success
* **/
private fun onRegisterSuccess() {
// navigate to welcome activity and finish it
val returnIntent = Intent()
(activity as LoginActivity).setResult(Activity.RESULT_OK, returnIntent)
(activity as LoginActivity).finish()
}

You only get a context once the fragment is attached to an activity.
When onCreateView is called you don't have a context yet and it returns:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentRegisterBinding.inflate(inflater, container, false)
context ?: return binding.root
// ...
}
You should move your set up logic to onViewCreated:
lateinit var binding: FragmentRegisterBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentRegisterBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
injectFeature()
setToolbar(binding)
subscribeUi(binding)
}

Related

BrowseSupportFragment(Leanback): manually select mainfragment

I am building app for android tv but having issue on manually selecting main fragment. I want click on header item to change main fragment rather than changing on focus using fragment factory.
Code
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isHeadersTransitionOnBackEnabled = true
val listRowPresenter = ListRowPresenter(FocusHighlight.ZOOM_FACTOR_SMALL)
mRowsAdapter = ArrayObjectAdapter(listRowPresenter)
adapter = mRowsAdapter
mainFragmentRegistry.registerFragment(PageRow::class.java, PageRowFragmentFactory())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loadData()
headersSupportFragment.setOnHeaderViewSelectedListener { viewHolder, row ->
//Only Focus View. Don't change main fragment
}
headersSupportFragment.setOnHeaderClickedListener { viewHolder, row ->
//Change main fragment here
//No documentation provided what to write here
}
}
fun loadData() {
val headerItem1 = HeaderItem(1, "HEADER_NAME_1")
val pageRow1 = PageRow(headerItem1)
mRowsAdapter.add(pageRow1)
val headerItem2 = HeaderItem(2, "HEADER_NAME_2")
val pageRow2 = PageRow(headerItem2)
mRowsAdapter.add(pageRow2)
}
Factory
private class PageRowFragmentFactory : BrowseSupportFragment.FragmentFactory<Fragment>() {
override fun createFragment(rowObj: Any?): Fragment {
val row = rowObj as Row
return when (row.headerItem.id) {
1L -> SampleFragmentA()
2L -> SampleFragmentB()
else -> throw IllegalArgumentException(String.format("Invalid row %s", rowObj))
}
}
}

Listener not implemented when inflating dialog-Android

I'm trying to inflate a custom dialog in my "CreateShoppingListMenuFragment" I've followed android's documentation but seem to be having a problem with the Listener, I know FragmentManager() is deprecated and used both parentFragmentManager & child FragmentManager to no success, maybe it's related?
Here is the error message:
logo1200.shoppinglist, PID: 24194
java.lang.ClassCastException: com.camilogo1200.shoppinglist.presentation.MainActivity#11852bbmust implement ShoppingListNameRequestListener
at com.camilogo1200.shoppinglist.presentation.fragments.ShoppingListNameRequestDialog.onAttach(ShoppinListNameRequestDialog.kt:68)
at androidx.fragment.app.Fragment.performAttach(Fragment.java:2922)
at androidx.fragment.app.FragmentStateManager.attach(FragmentStateManager.java:464)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:275)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
This is my DialogFragment:
class ShoppingListNameRequestDialog : DialogFragment() {
private lateinit var listener: ShoppingListNameRequestListener
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
val inflater: LayoutInflater = requireActivity().layoutInflater
val requestNameView = inflater.inflate(R.layout.shopping_list_name_request_dialog, null)
val nameInput = requestNameView.findViewById<TextView>(R.id.shopping_list_dialog_input)
var listName = ""
builder.setView(requestNameView)
.setPositiveButton(R.string.save_shopping_list,
DialogInterface.OnClickListener {dialog, id ->
if(nameInput.text.toString() != "")
listName = nameInput.text.toString()
listener.onDialogPositiveClick(this,listName);
})
.setNegativeButton(R.string.cancel,
DialogInterface.OnClickListener{dialog, id ->
listener.onDialogNegativeClick(this)
})
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
interface ShoppingListNameRequestListener {
fun onDialogPositiveClick(dialog: DialogFragment,listName:String)
fun onDialogNegativeClick(dialog: DialogFragment)
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
listener = context as ShoppingListNameRequestListener
} catch (e: ClassCastException) {
throw ClassCastException((context.toString() +
"must implement ShoppingListNameRequestListener"))
}
}
This is my "CreateShoppingListMenuFragment" (the host fragment where I'm inflating the dialog):
class CreateShoppingListMenuFragment : Fragment(),
ShoppingListNameRequestDialog.ShoppingListNameRequestListener {
private lateinit var binding: FragmentCreateShoppingListMenuBinding
private val viewModel: CreateShoppingListMenuViewModel by activityViewModels()
private val args: CreateShoppingListMenuFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
layoutInflater,
R.layout.fragment_create_shopping_list_menu,
container,
false
)
binding.lifecycleOwner = this
viewModel.createItems()
viewModel.viewState.observe(viewLifecycleOwner, ::handleViewState)
val login = args.ownerName
val listId = args.listId
viewModel.setOwnerAndList(login, listId)
binding.createItemButton.setOnClickListener {
val directionToFragment =
CreateShoppingListMenuFragmentDirections.actionCreateShoppingListMenuFragmentToCreateItemMenuFragment(
login,
listId
)
Navigation.findNavController(binding.root).navigate(directionToFragment)
}
binding.completeShoppingListButton.setOnClickListener {
showNoticeDialog()
}
return binding.root
}
private fun showNoticeDialog() {
val dialog = ShoppingListNameRequestDialog()
dialog.show(parentFragmentManager, "ShoppingListNameRequestDialog")
}
override fun onDialogPositiveClick(dialog: DialogFragment,listName: String) {
val result = viewModel.saveShoppingList(listName)
Log.i("shoppingListResult", "$result")
// travel to final fragment sent shoppinglist as arg
}
override fun onDialogNegativeClick(dialog: DialogFragment) {
// User touched the dialog's negative button
}
private fun handleViewState(viewState: CreateShoppingListMenuViewState) {
when (viewState) {
is CreateShoppingListMenuViewState.ErrorViewState -> showError(viewState.exception as ShoppingException)
//is RegisterViewState.SuccessViewState ->showSuccess()
else -> showSuccess(viewState)
}
}
private fun showSuccess(viewState: CreateShoppingListMenuViewState) {
val receivedList = viewState as CreateShoppingListMenuViewState.SuccessViewState
val dataList = receivedList.data
val adapter = ShoppingListMenuAdapter(dataList, viewModel::changeItemCount)
binding.itemListArray.adapter = adapter
}
private fun showError(exception: ShoppingException) {
if (exception.idError as? ItemError == ItemError.NO_ITEMS_CREATED) {
val message = getString(R.string.no_items_created_error_messages)
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
}
Any help would be greatly appreciated!
The Context in onAttach(Context context) is context Activity fragment does not have its own Context .
The problem here is you are casting context to ShoppingListNameRequestListener for this to work your Activity needs to implement the listener .
To solve this problem there are several ways. if we go with your approach we can pass fragment instance as targetFragment and use it as listener inside the DialogFragment .
private fun showNoticeDialog() {
val dialog = ShoppingListNameRequestDialog()
dialog.setTargetFragment(this)
dialog.show(parentFragmentManager, "ShoppingListNameRequestDialog")
}
Then inside dialog you can do something like this .
class ShoppingListNameRequestDialog:DialogFragment(){
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
targetFragment?.let {
listener = it as ShoppingListNameRequestListener
}
}
}
However setTargetFragment is deprecated now. As a Alternate way you can do the same with a shared ViewModel or with the new API FragmentResultListener.

Algolia Android - Empty query not being filtered out

I'm attempting to set my search box to not return any search results when the query is empty - i.e. when nothing has been typed in the box. Algolia InstantSearch by default returns all entries to scroll through which are then filtered as the user searches.
I followed the API docs on aloglia's website for removing the empty query but my search box still returns all entries. I'm a little stuck since it seems to be a very straightforward class, but using the default SearchBoxView vs my amended version NoEmptySearchBox makes no difference in results.
Here's GroupFragment where I'm calling the SearchBoxView:
class GroupFragment : Fragment() {
private val connection = ConnectionHandler()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.group_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel = ViewModelProviders.of(requireActivity())[SearcherViewModel::class.java]
val searchBoxView = NoEmptySearchBox(searchView)
viewModel.groups.observe(this, Observer { hits -> viewModel.adapterProduct.submitList(hits) })
connection += viewModel.searchBox.connectView(searchBoxView)
groupList.let {
it.itemAnimator = null
it.adapter = viewModel.adapterProduct
it.layoutManager = LinearLayoutManager(requireContext())
it.autoScrollToStart(viewModel.adapterProduct)
}
}
override fun onDestroyView() {
super.onDestroyView()
connection.disconnect()
}
}
And here's my NoEmptySearchBox class, which implements SearchBoxView:
class NoEmptySearchBox (
val searchView: SearchView
) : SearchBoxView {
override var onQueryChanged: Callback<String?>? = null
override var onQuerySubmitted: Callback<String?>? = null
init {
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
query?.isNotEmpty()?.let { onQuerySubmitted?.invoke(query) }
return false
}
override fun onQueryTextChange(query: String?): Boolean {
query?.isNotEmpty()?.let { onQuerySubmitted?.invoke(query) }
return false
}
})
}
override fun setText(text: String?, submitQuery: Boolean) {
searchView.setQuery(text, false)
}
}
And here's my SearcherViewModel:
class SearcherViewModel : ViewModel() {
val client = ClientSearch(ApplicationID("APP_ID"), APIKey("API_KEY"), LogLevel.ALL)
val index = client.initIndex(IndexName("groups"))
val searcher = SearcherSingleIndex(index)
override fun onCleared() {
super.onCleared()
searcher.cancel()
connection.disconnect()
}
val dataSourceFactory = SearcherSingleIndexDataSource.Factory(searcher) { hit ->
Group(
hit.json.getPrimitive("course_name").content,
hit.json.getObjectOrNull("_highlightResult")
)
}
val pagedListConfig = PagedList.Config.Builder().setPageSize(50).build()
val groups: LiveData<PagedList<Group>> = LivePagedListBuilder(dataSourceFactory, pagedListConfig).build()
val adapterProduct = GroupAdapter()
val searchBox = SearchBoxConnectorPagedList(searcher, listOf(groups))
val connection = ConnectionHandler()
init {
connection += searchBox
}
}

Saving and Restoring ListView (livedata) in Fragments

I'm trying to make a Todo app. I have succesfully implemented livedata and listview in fragments (fragments are default from the project quickstart template). My problem which I can't resolve is saving those todo's so they are still there upon launching app back again.
Browsed tons of answers on stack and blogs and read about whole lifecycle but I still don't get it. I finally gave up and this is what (not working) code I end up with atm:
FragmentLifeCycle to save "state" of the listOfToDoThings
class FragmentLifeCycle : Fragment() {
private var state: Parcelable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d("Lifecycle Info", "onCreate()")
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Log.d("Lifecycle Info", "onCreateView()")
return inflater.inflate(R.layout.activity_main, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Log.d("Lifecycle Info", "onActivityCreated()")
}
override fun onResume() {
super.onResume()
if (state != null) {
Log.i("Lifecycle Info", "onResume finally works")
listOfToDoThings.onRestoreInstanceState(state)
}
Log.d("Lifecycle Info", "onResume()")
}
override fun onPause() {
state = listOfToDoThings.onSaveInstanceState()
super.onPause()
Log.d("Lifecycle Info", "onStop()")
}
}
which throws nullpointer:
'android.os.Parcelable android.widget.ListView.onSaveInstanceState()' on a null object reference
And Main_Activity cleared out of tons of commented not-working solutions:
class MainActivity : AppCompatActivity(){
private var mSectionsPagerAdapter: SectionsPagerAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
mSectionsPagerAdapter = SectionsPagerAdapter(supportFragmentManager)
// Set up the ViewPager with the sections adapter.
container.adapter = mSectionsPagerAdapter
val fragmentManager = this.supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
val fragmentLifeCycle = FragmentLifeCycle()
fragmentTransaction.add(R.id.container, fragmentLifeCycle, "Lifecycle Fragment")
fragmentTransaction.commit()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
val id = item.itemId
if (id == R.id.action_settings) {
return true
}
return super.onOptionsItemSelected(item)
}
/**
* A [FragmentPagerAdapter] that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
inner class SectionsPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment {
// getItem is called to instantiate the fragment for the given page.
// Return a PlaceholderFragment (defined as a static inner class below).
return PlaceholderFragment.newInstance(position + 1)
}
override fun getCount(): Int {
// Show 3 total pages.
return 4
}
}
/**
* A placeholder fragment containing a simple view.
*/
class PlaceholderFragment : Fragment(), Renderer<TodoModel> {
private lateinit var store: TodoStore
override fun render(model: LiveData<TodoModel>) {
model.observe(this, Observer { newState ->
listOfToDoThings.adapter = TodoAdapter(requireContext(), newState?.todos ?: listOf())
})
}
private fun openDialog() {
val options = resources.getStringArray(R.array.filter_options).asList()
requireContext().selector(getString(R.string.filter_title), options) { _, i ->
val visible = when (i) {
1 -> Visibility.Active()
2 -> Visibility.Completed()
else -> Visibility.All()
}
store.dispatch(SetVisibility(visible))
}
}
private val mapStateToProps = Function<TodoModel, TodoModel> {
val keep: (Todo) -> Boolean = when(it.visibility) {
is Visibility.All -> {_ -> true}
is Visibility.Active -> {t: Todo -> !t.status}
is Visibility.Completed -> {t: Todo -> t.status}
}
return#Function it.copy(todos = it.todos.filter { keep(it) })
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.fragment_main, container, false)
rootView.section_label.text = getString(R.string.section_format, arguments?.getInt(ARG_SECTION_NUMBER))
#SuppressLint("SetTextI18n")
when(arguments?.getInt(ARG_SECTION_NUMBER)) {
1 -> rootView.section_name.text = "Daily Life"
2 -> rootView.section_name.text = "Work and College"
3 -> rootView.section_name.text = "Visits"
4 -> rootView.section_name.text = "Shop"
}
store = ViewModelProviders.of(this).get(TodoStore::class.java)
store.subscribe(this, mapStateToProps)
// Add task and then reset editText component
rootView.addNewToDo.setOnClickListener {
store.dispatch(AddTodo(editText.text.toString()))
editText.text = null
}
rootView.filter.setOnClickListener{ openDialog() }
// Press to change status of task
rootView.listOfToDoThings.adapter = TodoAdapter(requireContext(), listOf())
rootView.listOfToDoThings.setOnItemClickListener { _, _, _, id ->
store.dispatch(ToggleTodo(id))
}
// Hold to delete task
rootView.listOfToDoThings.setOnItemLongClickListener { _, _, _, id ->
store.dispatch(RemoveTodo(id))
true
}
return rootView
}
companion object {
/**
* The fragment argument representing the section number for this
* fragment.
*/
private val ARG_SECTION_NUMBER = "section_number"
/**
* Returns a new instance of this fragment for the given section
* number.
*/
fun newInstance(sectionNumber: Int): PlaceholderFragment {
val fragment = PlaceholderFragment()
val args = Bundle()
args.putInt(ARG_SECTION_NUMBER, sectionNumber)
fragment.arguments = args
return fragment
}
}
}
}
Not sure if its usefull but that's how TodoStore.kt looks like:
class TodoStore : Store<TodoModel>, ViewModel(){
private val state: MutableLiveData<TodoModel> = MutableLiveData()
// Start with all tasks visible regardless of previous state
private val initState = TodoModel(listOf(), Visibility.All())
override fun dispatch(action: Action) {
state.value = reduce(state.value, action)
}
private fun reduce(state: TodoModel?, action: Action): TodoModel {
val newState= state ?: initState
return when(action){
// Adds stuff upon creating new todo
is AddTodo -> newState.copy(
todos = newState.todos.toMutableList().apply {
add(Todo(action.text, action.id))
}
)
is ToggleTodo -> newState.copy(
todos = newState.todos.map {
if (it.id == action.id) {
it.copy(status = !it.status)
} else it
} as MutableList<Todo>
)
is SetVisibility -> newState.copy(
visibility = action.visibility
)
is RemoveTodo -> newState.copy(
todos = newState.todos.filter {
it.id != action.id
} as MutableList<Todo>
)
}
}
override fun subscribe(renderer: Renderer<TodoModel>, func: Function<TodoModel, TodoModel>) {
renderer.render(Transformations.map(state, func))
}
}
If I understand correctly you need to add a persistence layer to your application.
Try to use Room Database when load the ListView.
SavedInstanceState has some limitations and it should not be used to save a large amount of data or complex objects.
Android Persistence
Room Database
Hope this help.
If you need to save the position that the user is in the listView, save only the Int in a bundle on the method onSaveInstanceState() of the fragment. If you want to save the data inside the listView, you do not need to do this, because Android already did that, you just need to put the loadData (your code that init the data and set an adapter to the listView) in onActivityCreated and just restore the position in onViewStateRestored().

Can't access fragment's method from activity - UninitializedPropertyAccessException

I have a ViewPager. I want to change page when fragment allow it to do.
This is the onClick when I want to check.
when (currentPage) {
0 -> {
if ((mAdapter.getItem(currentPage) as NameFragment).canGo()) {
mViewPager.setCurrentItem(currentPage + 1, true)
}
}
1 -> ...
}
My Fragment:
private lateinit var fName: EditText
private lateinit var lName: EditText
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = UI {
verticalLayout {
fName = editText {
hint = "FIRST NAME"
textSize = 20f
}
lName = editText {
hint = "LAST NAME"
textSize = 20f
}
}
}
fun canGo(): Boolean {
val firstName = fName.text.toString()
val lastName = lName.text.toString()
if (firstName.isEmpty() || firstName.isBlank() || lastName.isBlank() || lastName.isEmpty()) {
toast("First name or Last name cannot be empty")
return false
}
return true
}
I initialize fName and lName on onCreateView method and this is the error I get. Full exception is here.
kotlin.UninitializedPropertyAccessException: lateinit property fName has not been initialized
These properties need to be initialised in the onViewCreated method, not onCreateView.
onCreateView is only responsible for creating the view as the name suggests, while onViewCreated is responsible for handling events after your fragment is properly created.
And if you need, you can create a callback that will be triggered when your fragment view is created, then you implement that callback in the activity, so that you will only access that canGo method after the fragment is created. Again, only if you really need to.
interface OnFragmentInflatedListener {
fun onFragmentInflated(fragment: Fragment)
}
// Fragment
private var onFragmentInflatedListener: OnFragmentInflatedListener? = null
fun setOnFragmentInflatedListener(onFragmentInflatedListener: OnFragmentInflatedListener?) {
this.onFragmentInflatedListener = onFragmentInflatedListener
}
// Fragment's onViewCreated
// initialise your properties here
onFragmentInflatedListener?.onFragmentInflated(this)
// Activity
class MyActivity : AppCompatActivity(), OnFragmentInflatedListener {
#Override
fun onCreate(savedInstanceState: Bundle) {
// do your stuff
myFragment.setOnFragmentInflatedListener(this)
}
#Override
fun onFragmentInflated(fragment: Fragment) {
// do what you wanted to do
}
}
I found the answer on this article. First I edited my Adapter:
private class Adapter(fm: FragmentManager, private val fragments: ArrayList<Fragment>) : FragmentStatePagerAdapter(fm) {
private val instantiatedFragments = SparseArray<WeakReference<Fragment>>()
override fun getItem(position: Int): Fragment = fragments[position]
override fun getCount() = fragments.size
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val fragment = super.instantiateItem(container, position) as Fragment
instantiatedFragments.put(position, WeakReference(fragment))
return fragment
}
#Nullable
fun getFragment(position: Int) = instantiatedFragments.get(position)?.get()
}
And also my onClick method
when (currentPage) {
0 -> {
if ((mAdapter.getFragment(currentPage) as NameFragment).canGo()) {
mViewPager.setCurrentItem(currentPage + 1, true)
}
}
1 -> ...
}
I changed getItem(currentPage) to getFragment(currentPage).

Categories

Resources