Skip to content

Commit

Permalink
fix(example): fix android side of flutter example and refactor exampl…
Browse files Browse the repository at this point in the history
…es (#766)

* fix(example): fix android side of flutter example

* java 17

* update

* update gradle

* update gradle

* equatable

* update

* restructure

* restructure
  • Loading branch information
hoc081098 authored Sep 3, 2024
1 parent 7a15bdd commit ba04c39
Show file tree
Hide file tree
Showing 29 changed files with 271 additions and 183 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/flutter-example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
java-version: '17'

- uses: subosito/[email protected]
with:
Expand Down
7 changes: 7 additions & 0 deletions examples/flutter/github_search/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
include: package:flutter_lints/flutter.yaml

analyzer:
language:
strict-casts: true
strict-raw-types: true
strict-inference: true

linter:
rules:
- prefer_final_locals
Expand Down
27 changes: 15 additions & 12 deletions examples/flutter/github_search/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}

kotlin {
jvmToolchain {
languageVersion = JavaLanguageVersion.of(17)
vendor = JvmVendorSpec.AZUL
}
}

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
Expand All @@ -6,11 +19,6 @@ if (localPropertiesFile.exists()) {
}
}

def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
Expand All @@ -21,12 +29,9 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
compileSdkVersion flutter.compileSdkVersion
namespace "com.example.github_search"

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
Expand Down Expand Up @@ -67,6 +72,4 @@ flutter {
source '../..'
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
dependencies {}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.github_search">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ package com.example.github_search

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
}
class MainActivity: FlutterActivity()
17 changes: 2 additions & 15 deletions examples/flutter/github_search/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,16 +1,3 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
Expand All @@ -26,6 +13,6 @@ subprojects {
project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
32 changes: 24 additions & 8 deletions examples/flutter/github_search/android/settings.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
include ':app'
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()

def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}

def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.5.2" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0")
}

rootProject.name = "github_search"
include ":app"
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';

import 'package:equatable/equatable.dart';
import 'package:http/http.dart' as http;

class GithubApi {
Expand Down Expand Up @@ -32,35 +33,36 @@ class GithubApi {
Future<SearchResult> _fetchResults(String term) async {
final response = await client.get(Uri.parse('$baseUrl$term'));
final results = json.decode(response.body);
final items = (results['items'] as List<dynamic>?) ?? [];

return SearchResult.fromJson(results['items']);
return SearchResult.fromJson(items.cast<Map<String, dynamic>>());
}
}

class SearchResult {
class SearchResult extends Equatable {
final List<SearchResultItem> items;

SearchResult(this.items);

factory SearchResult.fromJson(dynamic json) {
final items = (json as List)
.map((item) => SearchResultItem.fromJson(item))
.toList(growable: false);
const SearchResult(this.items);

factory SearchResult.fromJson(List<Map<String, dynamic>> json) {
final items = json.map(SearchResultItem.fromJson).toList(growable: false);
return SearchResult(items);
}

bool get isPopulated => items.isNotEmpty;

bool get isEmpty => items.isEmpty;

@override
List<Object?> get props => [items];
}

class SearchResultItem {
class SearchResultItem extends Equatable {
final String fullName;
final String url;
final String avatarUrl;

SearchResultItem(this.fullName, this.url, this.avatarUrl);
const SearchResultItem(this.fullName, this.url, this.avatarUrl);

factory SearchResultItem.fromJson(Map<String, dynamic> json) {
return SearchResultItem(
Expand All @@ -69,4 +71,7 @@ class SearchResultItem {
(json['owner'] as Map<String, dynamic>)['avatar_url'] as String,
);
}

@override
List<Object?> get props => [fullName, url, avatarUrl];
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import 'dart:async';

import 'package:rxdart/rxdart.dart';
import 'package:rxdart_ext/state_stream.dart';

import 'github_api.dart';
import '../api/github_api.dart';
import 'search_state.dart';

class SearchBloc {
final Sink<String> onTextChanged;
final Stream<SearchState> state;
final StateStream<SearchState> state;

final StreamSubscription<void> _subscription;

factory SearchBloc(GithubApi api) {
final onTextChanged = PublishSubject<String>();
Expand All @@ -23,22 +25,25 @@ class SearchBloc {
// to the View.
.switchMap<SearchState>((String term) => _search(term, api))
// The initial state to deliver to the screen.
.startWith(SearchNoTerm());
.publishState(const SearchNoTerm());

final subscription = state.connect();

return SearchBloc._(onTextChanged, state);
return SearchBloc._(onTextChanged, state, subscription);
}

SearchBloc._(this.onTextChanged, this.state);
SearchBloc._(this.onTextChanged, this.state, this._subscription);

void dispose() {
_subscription.cancel();
onTextChanged.close();
}

static Stream<SearchState> _search(String term, GithubApi api) => term.isEmpty
? Stream.value(SearchNoTerm())
? Stream.value(const SearchNoTerm())
: Rx.fromCallable(() => api.search(term))
.map((result) =>
result.isEmpty ? SearchEmpty() : SearchPopulated(result))
.startWith(SearchLoading())
.onErrorReturn(SearchError());
result.isEmpty ? const SearchEmpty() : SearchPopulated(result))
.startWith(const SearchLoading())
.onErrorReturn(const SearchError());
}
55 changes: 55 additions & 0 deletions examples/flutter/github_search/lib/bloc/search_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';

import '../api/github_api.dart';

// The State represents the data the View requires. The View consumes a Stream
// of States. The view rebuilds every time the Stream emits a new State!
//
// The State Stream will emit new States depending on the situation: The
// initial state, loading states, the list of results, and any errors that
// happen.
//
// The State Stream responds to input from the View by accepting a
// Stream<String>. We call this Stream the onTextChanged "intent".
@immutable
sealed class SearchState extends Equatable {
const SearchState();
}

class SearchLoading extends SearchState {
const SearchLoading();

@override
List<Object?> get props => [];
}

class SearchError extends SearchState {
const SearchError() : super();

@override
List<Object?> get props => [];
}

class SearchNoTerm extends SearchState {
const SearchNoTerm();

@override
List<Object?> get props => [];
}

class SearchPopulated extends SearchState {
final SearchResult result;

const SearchPopulated(this.result);

@override
List<Object?> get props => [result];
}

class SearchEmpty extends SearchState {
const SearchEmpty();

@override
List<Object?> get props => [];
}
4 changes: 2 additions & 2 deletions examples/flutter/github_search/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';

import 'github_api.dart';
import 'search_widget.dart';
import 'api/github_api.dart';
import 'search_screen.dart';

void main() => runApp(SearchApp(api: GithubApi()));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import 'package:flutter/material.dart';

import 'empty_result_widget.dart';
import 'github_api.dart';
import 'search_bloc.dart';
import 'search_error_widget.dart';
import 'search_intro_widget.dart';
import 'search_loading_widget.dart';
import 'search_result_widget.dart';
import 'search_state.dart';
import 'api/github_api.dart';
import 'bloc/search_bloc.dart';
import 'bloc/search_state.dart';
import 'widget/empty_result_widget.dart';
import 'widget/search_error_widget.dart';
import 'widget/search_intro_widget.dart';
import 'widget/search_loading_widget.dart';
import 'widget/search_result_widget.dart';

// The View in a Stream-based architecture takes two arguments: The State Stream
// and the onTextChanged callback. In our case, the onTextChanged callback will
Expand Down Expand Up @@ -46,7 +46,7 @@ class SearchScreenState extends State<SearchScreen> {
Widget build(BuildContext context) {
return StreamBuilder<SearchState>(
stream: bloc.state,
initialData: SearchNoTerm(),
initialData: bloc.state.value,
builder: (BuildContext context, AsyncSnapshot<SearchState> snapshot) {
final state = snapshot.requireData;
return Scaffold(
Expand Down Expand Up @@ -115,17 +115,12 @@ class SearchScreenState extends State<SearchScreen> {
}

Widget _buildChild(SearchState state) {
switch (state) {
case SearchNoTerm():
return const SearchIntro();
case SearchEmpty():
return const EmptyWidget();
case SearchLoading():
return const LoadingWidget();
case SearchError():
return const SearchErrorWidget();
case SearchPopulated():
return SearchResultWidget(items: state.result.items);
}
return switch (state) {
SearchNoTerm() => const SearchIntro(),
SearchEmpty() => const EmptyWidget(),
SearchLoading() => const LoadingWidget(),
SearchError() => const SearchErrorWidget(),
SearchPopulated() => SearchResultWidget(items: state.result.items)
};
}
}
Loading

0 comments on commit ba04c39

Please sign in to comment.