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().
Related
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))
}
}
}
I have an activity that has a SearchView that I use to enter a query, my app then uses to query to access an API. My activity further contains a fragment, and within this fragment I have my observer.
Further I have my ViewModel, which makes the API call when given a query. However, my observer is never notified about the update, and thus my view never updates. Unless I call it directly from my ViewModel upon initiation. I'll show it specifically here:
ViewModel
class SearchViewModel : ViewModel() {
val booksResponse = MutableLiveData<MutableList<BookResponse>>()
val loading = MutableLiveData<Boolean>()
val error = MutableLiveData<String>()
init {
getBooks("How to talk to a widower")
}
fun getBooks(bookTitle: String) {
GoogleBooksService.api.getBooks(bookTitle).enqueue(object: Callback<ResponseWrapper<BookResponse>> {
override fun onFailure(call: Call<ResponseWrapper<BookResponse>>, t: Throwable) {
onError(t.localizedMessage)
}
override fun onResponse(
call: Call<ResponseWrapper<BookResponse>>,
response: Response<ResponseWrapper<BookResponse>>
) {
if (response.isSuccessful){
val books = response.body()
Log.w("2.0 getFeed > ", Gson().toJson(response.body()));
books?.let {
// booksList.add(books.items)
booksResponse.value = books.items
loading.value = false
error.value = null
Log.i("Content of livedata", booksResponse.getValue().toString())
}
}
}
})
}
private fun onError(message: String) {
error.value = message
loading.value = false
}
}
Query Submit/ Activity
class NavigationActivity : AppCompatActivity(), SearchView.OnQueryTextListener, BooksListFragment.TouchActionDelegate {
lateinit var searchView: SearchView
lateinit var viewModel: SearchViewModel
private val mOnNavigationItemSelectedListener =
BottomNavigationView.OnNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {R.id.navigation_search -> {
navigationView.getMenu().setGroupCheckable(0, true, true);
replaceFragment(SearchListFragment.newInstance())
return#OnNavigationItemSelectedListener true
}
R.id.navigation_books -> {
navigationView.getMenu().setGroupCheckable(0, true, true);
replaceFragment(BooksListFragment.newInstance())
return#OnNavigationItemSelectedListener true
}
}
false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
replaceFragment(SearchListFragment.newInstance())
navigationView.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
//Set action bar color
val actionBar: ActionBar?
actionBar = supportActionBar
val colorDrawable = ColorDrawable(Color.parseColor("#FFDAEBE9"))
// actionBar!!.setBackgroundDrawable(colorDrawable)
// actionBar.setTitle(("Bobs Books"))
setSupportActionBar(findViewById(R.id.my_toolbar))
viewModel = ViewModelProvider(this).get(SearchViewModel::class.java)
}
override fun onBackPressed() {
super.onBackPressed()
navigationView.getMenu().setGroupCheckable(0, true, true);
}
private fun replaceFragment(fragment: Fragment){
supportFragmentManager
.beginTransaction()
.replace(R.id.fragmentHolder, fragment)
.commit()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.book_search_menu, menu)
val searchItem = menu.findItem(R.id.action_search)
searchView = searchItem.actionView as SearchView
searchView.setOnQueryTextListener(this)
searchView.queryHint = "Search for book"
/*searchView.onActionViewExpanded()
searchView.clearFocus()*/
// searchView.setIconifiedByDefault(false)
return true
}
override fun onQueryTextSubmit(query: String): Boolean {
//replaces fragment if in BooksListFragment when searching
replaceFragment(SearchListFragment.newInstance())
val toast = Toast.makeText(
applicationContext,
query,
Toast.LENGTH_SHORT
)
toast.show()
searchView.setQuery("",false)
searchView.queryHint = "Search for book"
// viewModel.onAddBook(Book(title = query!!, rating = 5, pages = 329))
Log.i("Query fra text field", query)
// viewModel.getBooks(query)
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
return false
}
override fun launchBookFragment(bookId: Book) {
supportFragmentManager
.beginTransaction()
.replace(R.id.fragmentHolder, com.example.bobsbooks.create.BookFragment.newInstance(bookId.uid))
.addToBackStack(null)
.commit()
navigationView.getMenu().setGroupCheckable(0, false, true);
}
}
Fragment
class SearchListFragment : Fragment() {
lateinit var viewModel: SearchViewModel
lateinit var contentListView: SearchListView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_search_list, container, false).apply {
contentListView = this as SearchListView
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindViewModel()
setContentView()
}
private fun setContentView(){
contentListView.initView()
}
private fun bindViewModel(){
Log.i("ViewmodelCalled", "BindViewModel has been called")
viewModel = ViewModelProvider(this).get(SearchViewModel::class.java)
viewModel.booksResponse.observe(viewLifecycleOwner, Observer {list ->
list?.let {
Log.i("Observer gets called", "Updatelistgetscalled")
contentListView.updateList(list)
}
} )
viewModel.error.observe(viewLifecycleOwner, Observer { errorMsg ->
})
viewModel.loading.observe(viewLifecycleOwner, Observer { isLoading ->
})
}
companion object {
fun newInstance(): SearchListFragment {
return SearchListFragment()
}
}
When I put the getBooks call into my Viewmodel Init, it will do everything correctly. It gets the bookresponse through the API, adds it to my LiveData and notifies my adapter.
However, if I instead delete that and call it through my Querysubmit in my Activity, it will, according to my logs, get the data and put it into my booksReponse:LiveData, but thats all it does. The observer is never notifed of this change, and thus the adapter never knows that it has new data to populate its views.
I feel like I've tried everything, I even have basically the same code working in another app, where it runs entirely in an activity instead of making the query in an activity, and rest is called in my fragment. My best guess is this has an impact, but I cant figure out how.
As per your explanation
However, if I instead delete that and call it through my Querysubmit in my Activity, it will, according to my logs, get the data and put it into my booksReponse:LiveData, but thats all it does. The observer is never notifed of this change, and thus the adapter never knows that it has new data to populate its views.
the problem is you are initializing SearchViewModel in both activity & fragment, so fragment doesn't have the same instance of SearchViewModel instead you should use shared viewmodel in fragment like :
viewModel = ViewModelProvider(requireActivity()).get(SearchViewModel::class.java)
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)
}
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).
Hello everyone I made a Drawer Activity but in my fragment i want a button So that when i press button this Should change text to Hi or Hello, but i don't where the code should be present when i try to add code in FirstFragemt.kt file it throw an error ,Please Help me Thank you
Here is My MainActivity :
class MainActivity : AppCompatActivity(),FirstFragment.OnFragmentInteractionListener,SecondFragment.OnFragmentInteractionListener, NavigationView.OnNavigationItemSelectedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
fab.setOnClickListener { view ->
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show()
}
val toggle = ActionBarDrawerToggle(this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
drawer_layout.addDrawerListener(toggle)
toggle.syncState()
nav_view.setNavigationItemSelectedListener(this)
}
override fun onBackPressed() {
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
drawer_layout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.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.
when (item.itemId) {
R.id.action_settings -> return true
else -> return super.onOptionsItemSelected(item)
}
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Handle navigation view item clicks here.
when (item.itemId) {
R.id.first_activity -> {
title = "Fragment One"
val fragment = FirstFragment()
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragmentID, fragment, "FragmentOne") //create first framelayout with id fram in the activity where fragments will be displayed
fragmentTransaction.commit() // Handle the camera action
}
R.id.second_activity -> {
title = "Fragment Second"
val fragment = SecondFragment()
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragmentID, fragment, "FragmentSecond") //create first framelayout with id fram in the activity where fragments will be displayed
fragmentTransaction.commit()
}
R.id.nav_share -> {
}
R.id.nav_exit -> {
System.exit(0)
}
}
drawer_layout.closeDrawer(GravityCompat.START)
return true
}
override fun onFragmentInteraction(uri: Uri) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
} }
Here is My FirstFragent file :
`
class FirstFragment : Fragment() {
private var mParam1: String? = null
private var mParam2: String? = null
private var mListener: OnFragmentInteractionListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (arguments != null) {
mParam1 = arguments.getString(ARG_PARAM1)
mParam2 = arguments.getString(ARG_PARAM2)
}
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater!!.inflate(R.layout.fragment_first, container, false)
}
// TODO: Rename method, update argument and hook method into UI event
fun onButtonPressed(uri: Uri) {
if (mListener != null) {
mListener!!.onFragmentInteraction(uri)
}
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (context is OnFragmentInteractionListener) {
mListener = context
} else {
throw RuntimeException(context!!.toString() + " must implement OnFragmentInteractionListener")
}
}
override fun onDetach() {
super.onDetach()
mListener = null
}
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* to the activity and potentially other fragments contained in that
* activity.
*
*
* See the Android Training lesson [Communicating with Other Fragments](http://developer.android.com/training/basics/fragments/communicating.html) for more information.
*/
interface OnFragmentInteractionListener {
// TODO: Update argument type and name
fun onFragmentInteraction(uri: Uri)
}
companion object {
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private val ARG_PARAM1 = "param1"
private val ARG_PARAM2 = "param2"
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* #param param1 Parameter 1.
* #param param2 Parameter 2.
* #return A new instance of fragment FirstFragment.
*/
// TODO: Rename and change types and number of parameters
fun newInstance(param1: String, param2: String): FirstFragment {
val fragment = FirstFragment()
val args = Bundle()
args.putString(ARG_PARAM1, param1)
args.putString(ARG_PARAM2, param2)
fragment.arguments = args
return fragment
}
}
}// Required empty public constructor
Here is my FirstFragment layout xml file :
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.a3.aakap.ftrial.FirstFragment">
<!-- TODO: Update blank fragment layout -->
<TextView
android:id="#+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="#string/hello_blank_fragment" />
<Button
android:id="#+id/button"
android:layout_width="200sp"
android:layout_height="50sp"
android:text="Click me !!"
android:layout_marginLeft="100dp"
android:layout_marginRight="100dp"
android:layout_gravity="center"/>
</FrameLayout>
create a overide of onViewCreated in those Fragment Here is a code :
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
button.setOnClickListener{
text.setText("Hello")
}
}
So, what you're missing is getting hold on the view of your fragment, and the views inside it (mainly your button and your TextView), and doing stuff on them.
In your FirstFragment.kt :
private lateinit var mTextView: TextView
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
val view = inflater!!.inflate(R.layout.fragment_first, container, false)
mTextView = findViewById(R.id.text)
findViewById<Button>(R.id.button).setOnClickListener { mTextView.text = "Hi" }
return view
}
class FirstFragment : Fragment() {
private var ctx: Context? = null
private var self: View? = null
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
ctx = container?.context
self = LayoutInflater.from(ctx).inflate(R.layout.fragment_start, container, false)
val bDaButton = self?.findViewById<Button>(R.id.bDaButton)
bDaButton?.setOnClickListener {
Toast.makeText(ctx, "button works!", Toast.LENGTH_SHORT).show()
}
return self
}
}
<Button
android:id="#+id/bDaButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Work!"/>