Search Cities Example
In this example, we auto-complete cities we type in a text field. When the city text field changes, we perform a network request. This network request will be done using an AsyncAction. (Check the Search cities application source code on GitHub)
Our application state looks like:
struct SearchCities {
var cities: [String]
}
class SearchCities(cities: List<String>) {
private val cities: List<String>
init {
this.cities = ArrayList(cities)
}
fun getCities(): List<String> {
return ArrayList(cities)
}
}
class SearchCities {
private final List<String> cities;
SearchCities(List<String> cities) {
this.cities = new ArrayList<>(cities);
}
List<String> getCities() {
return new ArrayList<>(cities);
}
}
We need to define an AsyncAction
that fetches the cities from the network. In this async action we perform a URLSession
data task request to https://autocomplete.wunderground.com/
with our query string.
When the request completes, we parse the received data, prepare a list of cities and dispatch an action that contains this list.
Let's see how that looks in code:
struct FetchCityAsyncAction: AsyncAction {
let query: String
init(query: String) {
self.query = query
}
func execute(getState: @escaping GetStateFunction, dispatch: @escaping DispatchFunction) {
let url = URL(string: "https://autocomplete.wunderground.com/aq?query=\(query)")!
// Perform the URL Requeus with URLSession
URLSession(configuration: .default).dataTask(with: url) { data, response, error in
// Get the response
let resp = try! JSONSerialization.jsonObject(with: data!, options: []) as! [String: Any]
let result = resp["RESULTS"] as! [[String: Any]]
// Prepare a list of cities
let cities = result.map({ $0["name"] as! String })
// Now we have the cities
// We dispatch the cities
dispatch(CitiesFetchedAction(cities: cities))
}.resume()
}
}
class FetchCityAsyncAction(private val query: String) : AsyncAction {
override fun execute(dispatcher: Dispatcher, getState: GetState) {
SearchCitiesService.searchCitiesAsync(query) { cities ->
val citiesFetchedAction = CitiesFetchedAction(cities)
dispatcher.dispatch(citiesFetchedAction)
}
}
}
class FetchCityAsyncAction implements AsyncAction {
private final String query;
FetchCityAsyncAction(String query) {
this.query = query;
}
@Override
public void execute(Dispatcher dispatcher, GetState getState) {
SearchCitiesService.searchCitiesAsync(query, cities -> {
Action citiesFetchedAction = new CitiesFetchedAction(cities);
dispatcher.dispatch(citiesFetchedAction);
});
}
}
The CitiesFetchedAction
is an action that contains the list of cities:
struct CitiesFetchedAction: Action {
let cities: [String]
}
class CitiesFetchedAction(cities: List<String>?) : Action<List<String>>(ACTION_TYPE, cities) {
companion object {
private val ACTION_TYPE = "CitiesFetchedAction"
}
}
class CitiesFetchedAction extends Action<List<String>> {
private static final String ACTION_TYPE = "CitiesFetchedAction";
CitiesFetchedAction(@Nullable List<String> cities) {
super(ACTION_TYPE, cities);
}
}
The reducer for this example only reduces CitiesFetchedAction
. In this simple example, it takes the cities
from the CitiesFetchedAction
and fills the SearchCities
State
with it.
struct SearchCitiesReducer: Reducer {
var initialState = SearchCities(cities: [])
func reduce(state: SearchCities, action: Action) -> SearchCities? {
// Handle each action
if let action = action as? CitiesFetchedAction {
return SearchCities(cities: action.cities)
}
// Important: If action does not affec the state, return nil
return nil
}
}
class SearchCitiesReducer : Reducer<SearchCities>() {
override fun reduce(
state: SearchCities,
action: Action<*>): SearchCities? {
return if (action is CitiesFetchedAction) {
SearchCities(action.getData<List<String>>()!!)
} else null
}
override fun getInitialState(): SearchCities {
return SearchCities(ArrayList())
}
}
class SearchCitiesReducer extends Reducer<SearchCities> {
@Nullable
@Override
public SearchCities reduce(@NonNull SearchCities state,
@NonNull Action<?> action) {
if (action instanceof CitiesFetchedAction) {
return new SearchCities(action.getData());
}
return null;
}
@NonNull
@Override
public SearchCities getInitialState() {
return new SearchCities(new ArrayList<>());
}
}
Now that we have the Reducer
, the Action
and the State
its time to create a store with them.
let store = Suas.createStore(reducer: SearchCitiesReducer(), middleware: AsyncMiddleware())
val store = Suas.createStore(SearchCitiesReducer())
.withMiddleware(AsyncMiddleware())
.build()
Store store = Suas.createStore(new SearchCitiesReducer())
.withMiddleware(new AsyncMiddleware())
.build();
Notice that we also must use pass the AsyncMiddleware
to handle our FetchCityAsyncAction
async action.
Finally, let's take a look at our UI: SearchCitiesViewController
.
class SearchCitiesViewController: UIViewController {
@IBOutlet weak var resultTextView: UITextView!
var listenerSubscription: Subscription<SearchCities>?
override func viewDidLoad() {
// Add a listener
listenerSubscription = store.addListener(forStateType: SearchCities.self) { [weak self] state in
// Notice that we capture self weakly to prevent strong memory cycles
// Combine the cities and set them in the result text view
self.resultTextView.text = state.cities.joined(separator: "\n")
}
}
@IBAction func textChanged(_ sender: Any) {
let textField = sender as! UITextField
// When text change dispatch the async function
store.dispatch(action: FetchCityAsyncAction(query: textField.text ?? ""))
}
deinit {
// Finally, dont forget to remove the listener
listenerSubscription?.removeListener()
}
}
class SearchCitiesActivity : Activity(), Listener<SearchCities> {
private var store: Store? = null
private var citiesAdapter: CitiesAdapter? = null
private var cityInput: EditText? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//...
cityInput = findViewById(R.id.city_field)
cityInput!!.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
val query = charSequence.toString()
val searchCitiesAction = AsyncMiddleware.create(FetchCityAsyncAction(query))
store!!.dispatch(searchCitiesAction)
}
override fun afterTextChanged(editable: Editable) {}
})
//...
store = Suas.createStore(SearchCitiesReducer())
.withMiddleware(AsyncMiddleware())
.build()
//...
citiesAdapter = CitiesAdapter()
}
override fun onStart() {
super.onStart()
store!!.addListener(SearchCities::class.java, this)
}
override fun onStop() {
super.onStop()
store!!.removeListener(this)
}
override fun update(state: SearchCities) {
citiesAdapter!!.update(state)
}
}
public class SearchCitiesActivity extends Activity implements Listener<SearchCities> {
private Store store;
private CitiesAdapter citiesAdapter;
private EditText cityInput;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
cityInput = findViewById(R.id.city_field);
cityInput.addTextChangedListener(new TextWatcher() {
//...
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
String query = charSequence.toString();
Action searchCitiesAction = AsyncMiddleware.create(new FetchCityAsyncAction(query));
store.dispatch(searchCitiesAction);
}
});
//...
store = Suas.createStore(new SearchCitiesReducer())
.withMiddleware(new AsyncMiddleware())
.build();
//...
citiesAdapter = new CitiesAdapter();
}
@Override
protected void onStart() {
super.onStart();
store.addListener(SearchCities.class, this);
}
@Override
protected void onStop() {
super.onStop();
store.removeListener(this);
}
@Override
public void update(@NonNull SearchCities state) {
citiesAdapter.update(state);
}
}
When the text changes in our search cities text field, we dispatch our FetchCityAsyncAction
async action. This action, when executed, will fetch the cities from the network and eventually. When the network request is completed it triggers a CitiesFetchedAction
which alters with the fetched cities.
Notice that we hold a reference to the subscription with listenerSubscription
. We will need it in the future to remove the listener when the view controller is deallocated.
When SearchCitiesViewController
is deallocated (deinit
is called) we removed the listener by calling listenerSubscription?.removeListener()
.
What's Next
Check URLSessionAsyncAction GitHub gist example. Also check the full Search cities application source code on GitHub.
Related Topics
Check these three examples on how to implement AsyncAction
:
- URLSessionAsyncAction to dispatch an action performs a network request asynchronously.
- DiskReadActionCompletionBlock to dispatch an action that reads from disk asynchronously.
- DiskWriteAsyncAction to dispatch an action that writes to disk asynchronously.
Other sample apps
List of sample applications
Counter App Example
Todo App Example
Todo app with settings example
Updated about 6 years ago