Kotlin - idiomatic way to create a Fragment newInstance pattern - android

The best practice on Android for creating a Fragment is to use a static factory method and pass arguments in a Bundle via setArguments().
In Java, this is done something like:
public class MyFragment extends Fragment {
static MyFragment newInstance(int foo) {
Bundle args = new Bundle();
args.putInt("foo", foo);
MyFragment fragment = new MyFragment();
fragment.setArguments(args);
return fragment;
}
}
In Kotlin this converts to:
class MyFragment : Fragment() {
companion object {
fun newInstance(foo: Int): MyFragment {
val args = Bundle()
args.putInt("foo", foo)
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
}
}
This makes sense to support interop with Java so it can still be called via MyFragment.newInstance(...), but is there a more idiomatic way to do this in Kotlin if we don't need to worry about Java interop?

I like to do it this way:
companion object {
private const val MY_BOOLEAN = "my_boolean"
private const val MY_INT = "my_int"
fun newInstance(aBoolean: Boolean, anInt: Int) = MyFragment().apply {
arguments = Bundle(2).apply {
putBoolean(MY_BOOLEAN, aBoolean)
putInt(MY_INT, anInt)
}
}
}
Edit: with KotlinX extensions, you can also do this
companion object {
private const val MY_BOOLEAN = "my_boolean"
private const val MY_INT = "my_int"
fun newInstance(aBoolean: Boolean, anInt: Int) = MyFragment().apply {
arguments = bundleOf(
MY_BOOLEAN to aBoolean,
MY_INT to anInt)
}
}

inline fun <reified T : Fragment>
newFragmentInstance(vararg params: Pair<String, Any>) =
T::class.java.newInstance().apply {
arguments = bundleOf(*params)
}`
So it is used like that:
val fragment = newFragmentInstance<YourFragment>("key" to value)
Credit
bundleOf() can be taken from Anko

Late to the party, but I believe Idiomatically it should be something like this:
private const val FOO = "foo"
private const val BAR = "bar"
class MyFragment : Fragment() {
companion object {
fun newInstance(foo: Int, bar: String) = MyFragment().withArgs {
putInt(FOO, foo)
putString(BAR, bar)
}
}
}
With an extension like this:
inline fun <T : Fragment> T.withArgs(argsBuilder: Bundle.() -> Unit): T =
this.apply {
arguments = Bundle().apply(argsBuilder)
}
or
companion object {
fun newInstance(foo: Int, bar: String) = MyFragment().apply {
arguments = bundleOf(
FOO to foo,
BAR to bar
)
}
}
The key being that the private constants should not be part of the companion object.

companion object {
private const val NOTE_ID = "NOTE_ID"
fun newInstance(noteId: Int?) = AddNoteFragment().apply {
arguments =
Bundle().apply { putInt(NOTE_ID, noteId ?: Int.MIN_VALUE) }
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.let {
noteId = it.getInt(NOTE_ID)
}
}

Another way of doing this I found here
class MyFragment: Fragment(){
companion object{
private val ARG_CAUGHT = "myFragment_caught"
fun newInstance(caught: Pokemon):MyFragment{
val args: Bundle = Bundle()
args.putSerializable(ARG_CAUGHT, caught)
val fragment = MyFragment()
fragment.arguments = args
return fragment
}
...
}
...
}

More elegant way in my opinion
open class Instance<T : Fragment> {
#Suppress("UNCHECKED_CAST")
fun newInstance(vararg args: Pair<String, Any?>): T {
val cls = Class.forName(javaClass.name.substringBefore("$"))
return (cls.newInstance() as T).apply {
arguments = bundleOf(*args)
}
}
}
class MyFragment : Fragment() {
companion object : Instance<MyFragment>()
}
Keep in mind to add proguard rule to save constructor
-keepclassmembers class * extends androidx.fragment.app.Fragment {
<init>(...);
}
Or without reflection and proguard
open class Instance<T : Fragment>(private val cls: Class<T>) {
fun newInstance(vararg args: Pair<String, Any?>): T {
return cls.newInstance().apply {
arguments = bundleOf(*args)
}
}
}
class MyFragment : Fragment() {
companion object : Instance<MyFragment>(MyFragment::class.java)
}
Example of usage
val myFragment = MyFragment.newInstance("foo" to "bar)

Kotlin package-level function
What about about that kotlin says to use package level function instead of “static” method
MyFragment.kt
class MyFragment : Fragment() {
.....
}
fun MyFragmentNewInstance(): MyFragment {
return MyFragment()
}
MyActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) {
supportFragmentManager.beginTransaction()
.add(R.id.fragmentContainer, MyFragmentNewInstance())
.commit()
}
}

Related

How can i get my args.field from my activity to my viewmodel

I want to use my args.breweryName from my reviewActivity in my reviewViewModel but i don't know what's the best way to do it. I have tried using savestatehandle but i'm not experienced with that. I'm new to programming with kotlin so i would appreciate the help!
My ReviewActivity
class ReviewActivity : AppCompatActivity() {
val breweryText: TextView
get() = findViewById(R.id.breweryName)
private val args: ReviewActivityArgs by navArgs<ReviewActivityArgs>()
private var shownFragment: Fragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_review)
initView()
if (savedInstanceState == null) {
showFragment(ReviewFragment.newInstance())
}
breweryText.text = args.breweryName
}
fun test(): String {
return args.breweryName
}
private fun initView() {
val button : FloatingActionButton = findViewById(R.id.add)
showFragment(ReviewFragment.newInstance())
button.setOnClickListener { showSaveDialog() }
}
private fun showFragment(fragment: Fragment) {
val fragmentTransaction: FragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragmentHolder, fragment)
fragmentTransaction.commitNow()
shownFragment = fragment
}
private fun showSaveDialog() {
val dialogFragment: DialogFragment
val tag: String
dialogFragment = ReviewDialog.newInstance(null, null, args.breweryName)
tag = ReviewDialog.TAG_DIALOG_REVIEW_SAVE
dialogFragment.show(supportFragmentManager, tag)
}
My ReviewViewModel
class ReviewViewModel constructor(application: Application) : AndroidViewModel(application) {
private val reviewDao: ReviewDao = ReviewsDatabase.getDatabase(application).reviewDao()
private val userDao: UserDao = ReviewsDatabase.getDatabase(application).userDao()
val data = MutableLiveData<String>()
val reviewList: LiveData<List<Review>>
val userList: LiveData<List<User>>
fun data(item: String) {
data.value = item
}
init {
reviewList = reviewDao.allReviews
userList = userDao.allUsers
}
fun insert(vararg reviews: Review) {
reviewDao.insert(*reviews)
}
fun update(review: Review) {
reviewDao.update(review)
}
fun deleteAll() {
reviewDao.deleteAll()
}
}

MutableSharedFlow is being re collected again and again

I have the following ViewModel and Fragment -
class HeroesViewModel(private val heroesRepository: HeroesRepository) : ViewModel() {
private val internalUiState = MutableStateFlow<UiState>(UiState.Initial)
val uiState = internalUiState.asLiveData()
private val internalUiAction = MutableSharedFlow<UiAction>(1).apply {
tryEmit(UiAction.GetSuggestedList)
}
val uiAction = internalUiAction.asLiveData()
private val externalUiEvent = MutableSharedFlow<UiEvent>(1)
private val uiEvent = externalUiEvent.asSharedFlow()
init {
observeUiEvents()
}
private fun observeUiEvents() = viewModelScope.launch {
uiEvent.collect { event ->
when (event) {
is UiEvent.ListItemClicked -> {
navigateToHeroDetails(event.heroModel)
}
is UiEvent.SearchTextChanged -> {
getHeroesByName(event.searchText)
}
}
}
}
private fun navigateToHeroDetails(heroModel: HeroesListModel) =
submitAction(UiAction.NavigateToHeroesDetails(heroModel))
private fun getHeroesByName(name: String) = viewModelScope.launch(Dispatchers.IO) {
when (val response = heroesRepository.getHeroesByNameWithSuggestions(name)) {
is NetworkResponse.Success -> {
internalUiState.emit(UiState.Data(response.body as List<HeroesListModel>))
}
is NetworkResponse.Error -> {
response.error.message?.let { message ->
internalUiState.emit(UiState.Error(message))
}
}
else -> {}
}
}
fun getSuggestedHeroesList() = viewModelScope.launch(Dispatchers.IO) {
when (val response = heroesRepository.getSuggestedHeroesList(true)) {
is NetworkResponse.Success -> {
submitState(UiState.Data(response.body as List<HeroesListModel>))
}
is NetworkResponse.Error -> {
response.error.message?.let { message ->
submitState(UiState.Error(message))
}
}
else -> {}
}
}
private fun submitAction(uiAction: UiAction) = internalUiAction.tryEmit(uiAction)
private fun submitState(uiState: UiState) = viewModelScope.launch {
internalUiState.emit(uiState)
}
fun submitEvent(uiEvent: UiEvent) = externalUiEvent.tryEmit(uiEvent)
sealed class UiEvent {
data class SearchTextChanged(val searchText: String) : UiEvent()
data class ListItemClicked(val heroModel: HeroesListModel) : UiEvent()
}
sealed class UiState {
data class Data(val modelsListResponse: List<BaseHeroListModel>) : UiState()
data class Error(val errorMessage: String) : UiState()
object Initial : UiState()
}
sealed class UiAction {
data class NavigateToHeroesDetails(val heroModel: HeroesListModel) : UiAction()
object GetSuggestedList : UiAction()
}
}
class DashboardFragment : Fragment() {
//Class Variables - UI
private lateinit var binding: FragmentDashboardBinding
//Class Variables - Dependency Injection
private val heroesViewModel = get<HeroesViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentDashboardBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
init()
observeUiState()
observeUiAction()
}
private fun observeUiAction() = heroesViewModel.uiAction.observe(viewLifecycleOwner) { action ->
when(action){
is HeroesViewModel.UiAction.GetSuggestedList -> {
getSuggestedHeroesList()
}
is HeroesViewModel.UiAction.NavigateToHeroesDetails -> {
navigateToHeroesDetails(action.heroModel)
}
}
}
private fun init() {
binding.heroesSearchView.setOnQueryTextListener(object : OnSearchViewOnlyTextChangedListener() {
override fun onQueryTextChange(newText: String?): Boolean {
if (newText.isNullOrEmpty()) return false
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.SearchTextChanged(newText))
binding.progressBar.setVisiblyAsVisible()
return false
}
})
}
private fun observeUiState() = heroesViewModel.uiState.observe(viewLifecycleOwner) { uiAction ->
when (uiAction) {
is HeroesViewModel.UiState.Data -> {
showHeroesList(uiAction)
}
is HeroesViewModel.UiState.Error -> {
showGeneralError(uiAction)
}
HeroesViewModel.UiState.Initial -> Unit
}
}
private fun navigateToHeroesDetails(heroModel: HeroesListModel) =
findNavController().navigate(DashboardFragmentDirections.actionMainFragmentToHeroesDetailsFragment(heroModel))
private fun showHeroesList(result: HeroesViewModel.UiState.Data) {
binding.heroesList.setContent {
LazyColumn {
items(result.modelsListResponse.toList()) { model ->
if (model is HeroListSeparatorModel)
HeroesListSeparatorItem(model)
else if (model is HeroesListModel)
HeroesListItem(model) {
heroesViewModel.submitEvent(HeroesViewModel.UiEvent.ListItemClicked(model))
}
}
}
}
binding.progressBar.setVisiblyAsGone()
}
private fun showGeneralError(result: HeroesViewModel.UiState.Error) {
Toast.makeText(requireContext(), result.errorMessage, Toast.LENGTH_LONG).show()
binding.progressBar.setVisiblyAsGone()
}
private fun getSuggestedHeroesList() {
heroesViewModel.getSuggestedHeroesList()
binding.progressBar.setVisiblyAsVisible()
}
}
As you can see, I have the replayCache set to 1 in internalUiAction but the value keeps emitting itself. When I navigate using the navigateToHeroesDetails() method and go back using the navigation bar I immediately observe the last uiAction emitted value which is NavigateToHeroesDetails, causing me to navigate again and again to the heroes details screen. This is an endless loop of navigation.
As far as a hint for a solution, if I double tap the navigation 2 times quickly it does indeed go back to the first Fragment. Seems like I am missing something related to SharedFlow

how to get callback between fragments in kotlin

I am trying to get a callback from one child fragment to parent fragment in kotlin and then later call a function in parent fragment to manage other layouts inside the child fragment.
I know how to get callback from a fragment to the activity and access the function in the fragment in the callback we cannot do that in Kotlin because of companion objects.
Another way is to pass context of fragment to child fragment and implement the interface using fragment's content in onAttach() but I cannot pass the context of parent fragment via constructor as I am using factory static methods.
Any help will be highly appreciated.
Child Fragment : DriverStatusFragment.kt
companion object {
#JvmStatic
fun newInstance() =
DriverStatusFragment().apply {
arguments = Bundle().apply {
}
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is OnCurrentOrderStatusChanged) {
orderStatusChangedCallBack = context
}
}
private fun handleUserOnlineStatus(isOnline: Boolean) {
CustomPreferences.setBooleanPreference(context!!, PrefEntity.IS_ONLINE, isOnline)
when (isOnline) {
true -> {
binding.idOnlineStatus.text = getString(R.string.online)
binding.idOnlineStatusImage.setImageResource(R.drawable.circle_green)
//testing:
orderStatusChangedCallBack?.orderStatusChanged(CONSTANTS.NEW_ORDER)
}
false -> {
binding.idOnlineStatus.text = getString(R.string.offline)
binding.idOnlineStatusImage.setImageResource(R.drawable.circle_gray)
}
}
}
Parent Fragment : HomeFragment.kt
class HomeFragment : Fragment(), OnMapReadyCallback, OnCurrentOrderStatusChanged {
companion object {
#JvmStatic
fun newInstance() =
HomeFragment().apply {
arguments = Bundle().apply {
}
}
}
fun handleOrderStatus(status: String) {
when (status) {
CONSTANTS.IDLE -> {
replaceFragment(
DriverStatusFragment.newInstance(),
DriverStatusFragment::class.java.simpleName
)
}
CONSTANTS.NEW_ORDER -> {
replaceFragment(
NewOrderFragment.newInstance(false),
NewOrderFragment::class.java.simpleName
)
}
CONSTANTS.ORDER_ACCEPTED -> {
replaceFragment(
EnRouteFragment.newInstance(CONSTANTS.ORDER_ACCEPTED),
EnRouteFragment::class.java.simpleName
)
}
CONSTANTS.ARRIVED_AT_DROP_LOCATION -> {
replaceFragment(
OrderDeliveredFragment.newInstance(CONSTANTS.ARRIVED_AT_DROP_LOCATION),
OrderDeliveredFragment::class.java.simpleName
)
}
CONSTANTS.DELIVERED -> {
replaceFragment(
DriverStatusFragment.newInstance(),
DriverStatusFragment::class.java.simpleName
)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
this.context = context
}
**NOT RECEIVING CALLBACK HERE**
override fun orderStatusChanged(orderStatus: String) {
CommonUtils.showToast(context!!, "REAched here")
handleOrderStatus(orderStatus)
}
}

Android Studio Kotlin - Change EditText text from a fragment

I need to edit a editText which is on main_layout from a fragment instead of MainActivity().
I've tried inflating main_layout to fragment but that doesn't worked (editText doesn't change), then I've tried to create method
fun changeEditText(){
editText.setText(R.string.name)
}
but when I call it in my Fragment using
MainActivity().changeEditText()
it gives me this error:
java.lang.NullPointerException: Attempt to invoke virtual method 'android.content.pm.ApplicationInfo android.content.Context.getApplicationInfo()' on a null object reference
How can I edit this editText from fragment?
You can't initialize an activity via its constructors. You can call the method from fragment like below
((MainActivity)getActivity()).changeEditText();
You can call getActivity() from the Fragment to get a reference to its parent activity:
(activity as? MainActivity)?.changeEditText()
However, a better approach would be to use a listener so the fragment doesn't care what it's parent activity has implemented:
interface Listener {
fun onTextChanged()
}
fun changeEditText() {
editText.setText(R.string.name)
listener?.onTextChanged()
}
class MainActivity : AppCompatActivity() {
//override ......
fun changeText() {
EditText editText = findViewById(R.id.my_edittext)
edittext.text = "something"
}
}
class MyFragment : Fragment() {
private lateinit var hostActivity: AppCompatActivity
override fun onAttach(context: Context){
hostActivity = context as AppCompatActivity
}
override fun onViewCreated(){
hostActivity.changeText()
}
}
Or
class MainActivity :AppCompatActivity(), MyCallback() {
// override ......
override fun onTextChange(){
val editText = findViewById(R.id.my_edittext)
edittext.text = "something"
}
}
class MyFragment : Fragment() {
private lateinit var myCallback: MyCallback
override fun onAttach(context: Context){
myCallback = context as MyCallback
}
override fun onViewCreated(){
myCallback.onTextChange()
}
}
interface MyCallback {
fun onTextChange()
}
Or
class MainActivity :AppCompatActivity(), MyCallback() {
override fun onCreate(savedInstanceState: Bundle?){
val sharedViewmodel = ViewmodelProviders.of(this).get(SharedViewModel.class)
sharedViewmodel.text.observe(this, object: Observer<String> {
override fun onChanged(text: String?){
val editText = findViewById(R.id.my_edittext)
edittext.text = text
}
})
}
}
class MyFragment : Fragment() {
private lateinit var hostActivity: AppCompatActivity
override fun onAttach(context: Context){
hostActivity = context as AppCompatActivity
}
override fun onViewCreated(){
val sharedViewmodel = ViewmodelProviders.of(hostActivity).get(SharedViewModel.class)
sharedViewmodel.text = "My new text"
}
}
class sharedViewModel: ViewModel(){
private val textHolderLiveData = MutableLiveData<String>()
fun getText(): LiveData<String> {
return textHolderLiveData
}
fun setText(text: String) {
textHolderLiveData.value = text
}
}

Why l can`t set listener in Kotlin?

I have OnClickInterface (with method fun onClickShape()) Main.class, and FlipFragment.class and ImageView (which called image in my code). My goal is make listener for image.
interface OnClickInterface {
fun onClickShape()
}
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initTabs()
var flip = FlipFragment()
flip.listener = object : OnClickInterface {
override fun onClickShape() {
Log.d("MainActivity", "Shape Pressed")
ToastUtils.showSuccessMessage(baseContext, "sometext")
}
}
}
fun initTabs() {
var adapter = TabsPagerFragmentAdapter(supportFragmentManager)
mViewPager.adapter = adapter
mTabLayout.setupWithViewPager(mViewPager)
}
}
onCreate in FlipFragment
var image = view.findViewById<ImageView>(R.id.fShapeView)
image.setOnClickListener(View.OnClickListener {
Log.d("FlipFragment", "PRESSED")
if (listener != null)
listener!!.onClickShape()
})
App was loading well, without errors. But when I pressed in the image I show in my log FlipFragment: PRESSED that's mean that my application call method from FragmentFlip, not override method from MainActivity. Why?
I searched error . My app show NPE here.
flip.listener = object : OnClickInterface {
override fun onClickShape() {
Log.d("MainActivity", "Shape Pressed")
ToastUtils.showSuccessMessage(baseContext, "someText")
}}
Why listener = null . I defined it with anonymous class.
All code in FlipFragment
class FlipFragment : Fragment() {
private var layout = R.layout.view_flip
var listener: OnClickInterface? = null
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
var view: View
view = inflater!!.inflate(layout, container, false)
var image = view.findViewById<ImageView>(R.id.fShapeView)
image.setOnClickListener(View.OnClickListener {
Log.d("FlipFragment", "PRESSED")
if (listener != null){
listener!!.onClickShape()}
})
return view
}
companion object {
fun getInstanse(): FlipFragment {
var args = Bundle()
var flipFragment = FlipFragment()
flipFragment.arguments = args
return flipFragment
}
}
}
If you need all code it is FragmentPagerAdapter.class
class TabsPagerFragmentAdapter(fm: FragmentManager?) : FragmentPagerAdapter(fm) {
var tabs: Array<String> = arrayOf("Flip", "Multi")
override fun getItem(position: Int) = when(position){
0 -> FlipFragment.getInstanse()
1 -> Mulit.getInstanse() //it is empty now
else -> FlipFragment.getInstanse()
}
override fun getPageTitle(position: Int) = tabs[position]
override fun getCount() = tabs.size
}

Categories

Resources